From fd4a729181bdf2cefa66a326f077e12813029a15 Mon Sep 17 00:00:00 2001 From: Yue Guo Date: Fri, 7 May 2021 14:02:20 -0700 Subject: [PATCH 01/24] testing --- __tests__/implementation.test.js | 2057 +++++++++++++++--------------- examples/swapi/swapi-loaders.js | 21 +- src/codegen.ts | 3 + src/config.ts | 2 + src/genTypeFlow.ts | 3 +- src/implementation.ts | 54 +- src/runtimeHelpers.ts | 107 ++ 7 files changed, 1193 insertions(+), 1054 deletions(-) diff --git a/__tests__/implementation.test.js b/__tests__/implementation.test.js index c366e87..c7905d1 100644 --- a/__tests__/implementation.test.js +++ b/__tests__/implementation.test.js @@ -95,150 +95,150 @@ test('non batch endpoint', async () => { }); }); -test('batch endpoint', async () => { - const config = { - resources: { - foo: { - isBatchResource: true, - docsLink: 'example.com/docs/bar', - batchKey: 'foo_ids', - newKey: 'foo_id', - }, - }, - }; - - const resources = { - foo: ({ foo_ids }) => { - expect(foo_ids).toEqual([1, 2, 3]); - return Promise.resolve([ - { foo_id: 1, foo_value: 'hello' }, - { foo_id: 2, foo_value: 'world' }, - { foo_id: 3, foo_value: '!' }, - ]); - }, - }; - - await createDataLoaders(config, async (getLoaders) => { - const loaders = getLoaders(resources); - - const results = await loaders.foo.loadMany([{ foo_id: 1 }, { foo_id: 2 }, { foo_id: 3 }]); - expect(results).toEqual([ - { foo_id: 1, foo_value: 'hello' }, - { foo_id: 2, foo_value: 'world' }, - { foo_id: 3, foo_value: '!' }, - ]); - }); -}); - -test('batch endpoint (with reorderResultsByKey)', async () => { - const config = { - resources: { - foo: { - isBatchResource: true, - docsLink: 'example.com/docs/bar', - batchKey: 'foo_ids', - newKey: 'foo_id', - reorderResultsByKey: 'foo_id', - }, - }, - }; - - const resources = { - foo: ({ foo_ids }) => { - expect(foo_ids).toEqual([1, 2, 3]); - return Promise.resolve([ - { foo_id: 2, foo_value: 'world' }, - { foo_id: 1, foo_value: 'hello' }, - { foo_id: 3, foo_value: '!' }, - ]); - }, - }; - - await createDataLoaders(config, async (getLoaders) => { - const loaders = getLoaders(resources); - - const results = await loaders.foo.loadMany([{ foo_id: 1 }, { foo_id: 2 }, { foo_id: 3 }]); - expect(results).toEqual([ - { foo_id: 1, foo_value: 'hello' }, - { foo_id: 2, foo_value: 'world' }, - { foo_id: 3, foo_value: '!' }, - ]); - }); -}); - -test('batch endpoint (with nestedPath)', async () => { - const config = { - resources: { - foo: { - isBatchResource: true, - docsLink: 'example.com/docs/bar', - batchKey: 'foo_ids', - newKey: 'foo_id', - nestedPath: 'foos', - }, - }, - }; - - const resources = { - foo: ({ foo_ids }) => { - expect(foo_ids).toEqual([1, 2, 3]); - return Promise.resolve({ - foos: [ - { foo_id: 1, foo_value: 'hello' }, - { foo_id: 2, foo_value: 'world' }, - { foo_id: 3, foo_value: '!' }, - ], - }); - }, - }; - - await createDataLoaders(config, async (getLoaders) => { - const loaders = getLoaders(resources); - - const results = await loaders.foo.loadMany([{ foo_id: 1 }, { foo_id: 2 }, { foo_id: 3 }]); - expect(results).toEqual([ - { foo_id: 1, foo_value: 'hello' }, - { foo_id: 2, foo_value: 'world' }, - { foo_id: 3, foo_value: '!' }, - ]); - }); -}); - -test('batch endpoint (with commaSeparatedBatchKey)', async () => { - const config = { - resources: { - foo: { - isBatchResource: true, - docsLink: 'example.com/docs/bar', - batchKey: 'foo_ids', - newKey: 'foo_id', - commaSeparatedBatchKey: true, - }, - }, - }; - - const resources = { - foo: ({ foo_ids }) => { - expect(foo_ids).toEqual('1,2,3'); - return Promise.resolve([ - { foo_id: 1, foo_value: 'hello' }, - { foo_id: 2, foo_value: 'world' }, - { foo_id: 3, foo_value: '!' }, - ]); - }, - }; - - await createDataLoaders(config, async (getLoaders) => { - const loaders = getLoaders(resources); - - const results = await loaders.foo.loadMany([{ foo_id: 1 }, { foo_id: 2 }, { foo_id: 3 }]); - expect(results).toEqual([ - { foo_id: 1, foo_value: 'hello' }, - { foo_id: 2, foo_value: 'world' }, - { foo_id: 3, foo_value: '!' }, - ]); - }); -}); +// test('batch endpoint', async () => { +// const config = { +// resources: { +// foo: { +// isBatchResource: true, +// docsLink: 'example.com/docs/bar', +// batchKey: 'foo_ids', +// newKey: 'foo_id', +// }, +// }, +// }; + +// const resources = { +// foo: ({ foo_ids }) => { +// expect(foo_ids).toEqual([1, 2, 3]); +// return Promise.resolve([ +// { foo_id: 1, foo_value: 'hello' }, +// { foo_id: 2, foo_value: 'world' }, +// { foo_id: 3, foo_value: '!' }, +// ]); +// }, +// }; + +// await createDataLoaders(config, async (getLoaders) => { +// const loaders = getLoaders(resources); + +// const results = await loaders.foo.loadMany([{ foo_id: 1 }, { foo_id: 2 }, { foo_id: 3 }]); +// expect(results).toEqual([ +// { foo_id: 1, foo_value: 'hello' }, +// { foo_id: 2, foo_value: 'world' }, +// { foo_id: 3, foo_value: '!' }, +// ]); +// }); +// }); + +// test('batch endpoint (with reorderResultsByKey)', async () => { +// const config = { +// resources: { +// foo: { +// isBatchResource: true, +// docsLink: 'example.com/docs/bar', +// batchKey: 'foo_ids', +// newKey: 'foo_id', +// reorderResultsByKey: 'foo_id', +// }, +// }, +// }; + +// const resources = { +// foo: ({ foo_ids }) => { +// expect(foo_ids).toEqual([1, 2, 3]); +// return Promise.resolve([ +// { foo_id: 2, foo_value: 'world' }, +// { foo_id: 1, foo_value: 'hello' }, +// { foo_id: 3, foo_value: '!' }, +// ]); +// }, +// }; + +// await createDataLoaders(config, async (getLoaders) => { +// const loaders = getLoaders(resources); + +// const results = await loaders.foo.loadMany([{ foo_id: 1 }, { foo_id: 2 }, { foo_id: 3 }]); +// expect(results).toEqual([ +// { foo_id: 1, foo_value: 'hello' }, +// { foo_id: 2, foo_value: 'world' }, +// { foo_id: 3, foo_value: '!' }, +// ]); +// }); +// }); + +// test('batch endpoint (with nestedPath)', async () => { +// const config = { +// resources: { +// foo: { +// isBatchResource: true, +// docsLink: 'example.com/docs/bar', +// batchKey: 'foo_ids', +// newKey: 'foo_id', +// nestedPath: 'foos', +// }, +// }, +// }; + +// const resources = { +// foo: ({ foo_ids }) => { +// expect(foo_ids).toEqual([1, 2, 3]); +// return Promise.resolve({ +// foos: [ +// { foo_id: 1, foo_value: 'hello' }, +// { foo_id: 2, foo_value: 'world' }, +// { foo_id: 3, foo_value: '!' }, +// ], +// }); +// }, +// }; + +// await createDataLoaders(config, async (getLoaders) => { +// const loaders = getLoaders(resources); + +// const results = await loaders.foo.loadMany([{ foo_id: 1 }, { foo_id: 2 }, { foo_id: 3 }]); +// expect(results).toEqual([ +// { foo_id: 1, foo_value: 'hello' }, +// { foo_id: 2, foo_value: 'world' }, +// { foo_id: 3, foo_value: '!' }, +// ]); +// }); +// }); + +// test('batch endpoint (with commaSeparatedBatchKey)', async () => { +// const config = { +// resources: { +// foo: { +// isBatchResource: true, +// docsLink: 'example.com/docs/bar', +// batchKey: 'foo_ids', +// newKey: 'foo_id', +// commaSeparatedBatchKey: true, +// }, +// }, +// }; + +// const resources = { +// foo: ({ foo_ids }) => { +// expect(foo_ids).toEqual('1,2,3'); +// return Promise.resolve([ +// { foo_id: 1, foo_value: 'hello' }, +// { foo_id: 2, foo_value: 'world' }, +// { foo_id: 3, foo_value: '!' }, +// ]); +// }, +// }; + +// await createDataLoaders(config, async (getLoaders) => { +// const loaders = getLoaders(resources); + +// const results = await loaders.foo.loadMany([{ foo_id: 1 }, { foo_id: 2 }, { foo_id: 3 }]); +// expect(results).toEqual([ +// { foo_id: 1, foo_value: 'hello' }, +// { foo_id: 2, foo_value: 'world' }, +// { foo_id: 3, foo_value: '!' }, +// ]); +// }); +// }); test('batch endpoint (multiple requests)', async () => { const config = { @@ -248,17 +248,19 @@ test('batch endpoint (multiple requests)', async () => { docsLink: 'example.com/docs/bar', batchKey: 'foo_ids', newKey: 'foo_id', + secondBatchKey: 'properties', + secondNewKey: 'property', }, }, }; const resources = { - foo: ({ foo_ids, include_extra_info }) => { - if (_.isEqual(foo_ids, [1, 2])) { + foo: ({ foo_ids, properties, include_extra_info }) => { + if (_.isEqual(foo_ids, [2, 1])) { expect(include_extra_info).toBe(false); return Promise.resolve([ - { foo_id: 1, foo_value: 'hello' }, - { foo_id: 2, foo_value: 'world' }, + { foo_id: 1, photo: 'hello', id: 'world', name: 'greetings' }, + { foo_id: 2, photo: 'hello', id: 'world', name: 'greetings' }, ]); } @@ -267,7 +269,9 @@ test('batch endpoint (multiple requests)', async () => { return Promise.resolve([ { foo_id: 3, - foo_value: 'greetings', + photo: 'hello', + id: 'world', + name: 'greetings', extra_stuff: 'lorem ipsum', }, ]); @@ -279,888 +283,889 @@ test('batch endpoint (multiple requests)', async () => { const loaders = getLoaders(resources); const results = await loaders.foo.loadMany([ - { foo_id: 1, include_extra_info: false }, - { foo_id: 2, include_extra_info: false }, - { foo_id: 3, include_extra_info: true }, + { foo_id: 2, property: 'name', include_extra_info: false }, + { foo_id: 1, property: 'photo', include_extra_info: false }, + { foo_id: 3, property: 'photo', include_extra_info: true }, ]); expect(results).toEqual([ - { foo_id: 1, foo_value: 'hello' }, - { foo_id: 2, foo_value: 'world' }, - { foo_id: 3, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, - ]); - }); -}); - -test('batch endpoint that rejects', async () => { - const config = { - resources: { - foo: { - isBatchResource: true, - docsLink: 'example.com/docs/bar', - batchKey: 'foo_ids', - newKey: 'foo_id', - }, - }, - }; - - const resources = { - foo: ({ foo_ids, include_extra_info }) => { - if (_.isEqual(foo_ids, [1, 3])) { - expect(include_extra_info).toBe(false); - return Promise.reject('yikes'); - } - - if (_.isEqual(foo_ids, [2, 4, 5])) { - expect(include_extra_info).toBe(true); - return Promise.resolve([ - { - foo_id: 2, - foo_value: 'greetings', - extra_stuff: 'lorem ipsum', - }, - { - foo_id: 4, - foo_value: 'greetings', - extra_stuff: 'lorem ipsum', - }, - { - foo_id: 5, - foo_value: 'greetings', - extra_stuff: 'lorem ipsum', - }, - ]); - } - }, - }; - - await createDataLoaders(config, async (getLoaders) => { - const loaders = getLoaders(resources); + { foo_id: 1, photo: 'hello' }, - const results = await loaders.foo.loadMany([ - { foo_id: 1, include_extra_info: false }, - { foo_id: 2, include_extra_info: true }, - { foo_id: 3, include_extra_info: false }, - { foo_id: 4, include_extra_info: true }, - { foo_id: 5, include_extra_info: true }, - ]); - - // NonError comes from the default error handler which uses ensure-error - expect(results).toMatchObject([ - expect.toBeError(/yikes/, 'NonError'), - { foo_id: 2, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, - expect.toBeError(/yikes/, 'NonError'), - { foo_id: 4, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, - { foo_id: 5, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, - ]); - }); -}); - -test('batch endpoint (multiple requests, default error handling)', async () => { - const config = { - resources: { - foo: { - isBatchResource: true, - docsLink: 'example.com/docs/bar', - batchKey: 'foo_ids', - newKey: 'foo_id', - }, - }, - }; - - const resources = { - foo: ({ foo_ids, include_extra_info }) => { - if (_.isEqual(foo_ids, [1, 3])) { - expect(include_extra_info).toBe(false); - throw new Error('yikes'); - } - - if (_.isEqual(foo_ids, [2, 4, 5])) { - expect(include_extra_info).toBe(true); - return Promise.resolve([ - { - foo_id: 2, - foo_value: 'greetings', - extra_stuff: 'lorem ipsum', - }, - { - foo_id: 4, - foo_value: 'greetings', - extra_stuff: 'lorem ipsum', - }, - { - foo_id: 5, - foo_value: 'greetings', - extra_stuff: 'lorem ipsum', - }, - ]); - } - }, - }; - - await createDataLoaders(config, async (getLoaders) => { - const loaders = getLoaders(resources); - - const results = await loaders.foo.loadMany([ - { foo_id: 1, include_extra_info: false }, - { foo_id: 2, include_extra_info: true }, - { foo_id: 3, include_extra_info: false }, - { foo_id: 4, include_extra_info: true }, - { foo_id: 5, include_extra_info: true }, - ]); - - expect(results).toMatchObject([ - expect.toBeError(/yikes/), - { foo_id: 2, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, - expect.toBeError(/yikes/), - { foo_id: 4, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, - { foo_id: 5, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, - ]); - }); -}); - -test('batch endpoint (multiple requests, custom error handling)', async () => { - async function errorHandler(resourcePath, error) { - expect(resourcePath).toEqual(['foo']); - expect(error.message).toBe('yikes'); - return new Error('hello from custom error handler'); - } - - const config = { - resources: { - foo: { - isBatchResource: true, - docsLink: 'example.com/docs/bar', - batchKey: 'foo_ids', - newKey: 'foo_id', - }, - }, - }; - - const resources = { - foo: ({ foo_ids, include_extra_info }) => { - if (_.isEqual(foo_ids, [1, 3])) { - expect(include_extra_info).toBe(false); - throw new Error('yikes'); - } - - if (_.isEqual(foo_ids, [2, 4, 5])) { - expect(include_extra_info).toBe(true); - return Promise.resolve([ - { - foo_id: 2, - foo_value: 'greetings', - extra_stuff: 'lorem ipsum', - }, - { - foo_id: 4, - foo_value: 'greetings', - extra_stuff: 'lorem ipsum', - }, - { - foo_id: 5, - foo_value: 'greetings', - extra_stuff: 'lorem ipsum', - }, - ]); - } - }, - }; - - await createDataLoaders(config, async (getLoaders) => { - const loaders = getLoaders(resources, { errorHandler }); - - const results = await loaders.foo.loadMany([ - { foo_id: 1, include_extra_info: false }, - { foo_id: 2, include_extra_info: true }, - { foo_id: 3, include_extra_info: false }, - { foo_id: 4, include_extra_info: true }, - { foo_id: 5, include_extra_info: true }, - ]); - - expect(results).toMatchObject([ - expect.toBeError(/hello from custom error handler/), - { foo_id: 2, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, - expect.toBeError(/hello from custom error handler/), - { foo_id: 4, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, - { foo_id: 5, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, - ]); - }); -}); - -test('batch endpoint (multiple requests, error handling, nestedPath)', async () => { - const config = { - eek: true, - resources: { - foo: { - isBatchResource: true, - docsLink: 'example.com/docs/bar', - batchKey: 'foo_ids', - newKey: 'foo_id', - nestedPath: 'foo_data', - }, - }, - }; - - const resources = { - foo: ({ foo_ids, include_extra_info }) => { - if (_.isEqual(foo_ids, [1, 3])) { - expect(include_extra_info).toBe(false); - return new Error('yikes'); - } - - if (_.isEqual(foo_ids, [2, 4, 5])) { - expect(include_extra_info).toBe(true); - return Promise.resolve({ - foo_data: [ - { - foo_id: 2, - foo_value: 'greetings', - extra_stuff: 'lorem ipsum', - }, - { - foo_id: 4, - foo_value: 'greetings', - extra_stuff: 'lorem ipsum', - }, - { - foo_id: 5, - foo_value: 'greetings', - extra_stuff: 'lorem ipsum', - }, - ], - }); - } - }, - }; - - await createDataLoaders(config, async (getLoaders) => { - const loaders = getLoaders(resources); - - const results = await loaders.foo.loadMany([ - { foo_id: 1, include_extra_info: false }, - { foo_id: 2, include_extra_info: true }, - { foo_id: 3, include_extra_info: false }, - { foo_id: 4, include_extra_info: true }, - { foo_id: 5, include_extra_info: true }, - ]); - - expect(results).toMatchObject([ - expect.toBeError(/yikes/), - { foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, - expect.toBeError(/yikes/), - { foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, - { foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, + { foo_id: 3, name: 'greetings', extra_stuff: 'lorem ipsum' }, + { foo_id: 2, id: 'world' }, ]); }); }); -test('batch endpoint (multiple requests, error handling - non array response)', async () => { - const config = { - eek: true, - resources: { - foo: { - isBatchResource: true, - docsLink: 'example.com/docs/bar', - batchKey: 'foo_ids', - newKey: 'foo_id', - }, - }, - }; - - const resources = { - foo: ({ foo_ids, include_extra_info }) => { - if (_.isEqual(foo_ids, [1, 3])) { - expect(include_extra_info).toBe(false); - return new Error('yikes'); - } - - if (_.isEqual(foo_ids, [2, 4, 5])) { - expect(include_extra_info).toBe(true); - // Deliberately returning an object, not an array - return Promise.resolve({ - foo: 'bar', - }); - } - }, - }; - - await createDataLoaders(config, async (getLoaders) => { - const loaders = getLoaders(resources); - - const results = await loaders.foo.loadMany([ - { foo_id: 1, include_extra_info: false }, - { foo_id: 2, include_extra_info: true }, - { foo_id: 3, include_extra_info: false }, - { foo_id: 4, include_extra_info: true }, - { foo_id: 5, include_extra_info: true }, - ]); - - expect(results).toMatchObject([ - expect.toBeError('yikes'), - expect.toBeError('[dataloader-codegen :: foo] Expected response to be an array'), - expect.toBeError('yikes'), - expect.toBeError('[dataloader-codegen :: foo] Expected response to be an array'), - expect.toBeError('[dataloader-codegen :: foo] Expected response to be an array'), - ]); - }); -}); - -test('batch endpoint (multiple requests, error handling, with reordering)', async () => { - const config = { - resources: { - foo: { - isBatchResource: true, - docsLink: 'example.com/docs/bar', - batchKey: 'foo_ids', - newKey: 'foo_id', - reorderResultsByKey: 'foo_id', - }, - }, - }; - - const resources = { - foo: ({ foo_ids, include_extra_info }) => { - if (_.isEqual(foo_ids, [1, 3])) { - expect(include_extra_info).toBe(false); - return new Error('yikes'); - } - - if (_.isEqual(foo_ids, [2, 4, 5])) { - expect(include_extra_info).toBe(true); - // return items deliberately out of order - return Promise.resolve([ - { - foo_id: 4, - foo_value: 'greetings', - extra_stuff: 'lorem ipsum', - }, - { - foo_id: 5, - foo_value: 'greetings', - extra_stuff: 'lorem ipsum', - }, - { - foo_id: 2, - foo_value: 'greetings', - extra_stuff: 'lorem ipsum', - }, - ]); - } - }, - }; - - await createDataLoaders(config, async (getLoaders) => { - const loaders = getLoaders(resources); - - const results = await loaders.foo.loadMany([ - { foo_id: 1, include_extra_info: false }, - { foo_id: 2, include_extra_info: true }, - { foo_id: 3, include_extra_info: false }, - { foo_id: 4, include_extra_info: true }, - { foo_id: 5, include_extra_info: true }, - ]); - - expect(results).toMatchObject([ - expect.toBeError(/yikes/), - { foo_id: 2, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, - expect.toBeError(/yikes/), - { foo_id: 4, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, - { foo_id: 5, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, - ]); - }); -}); - -/** - * Without reorderResultsByKey: - * If we requested 3 items, but the resource only returns 2, we don't know which - * response is missing. It's unsafe to return any results, so we must throw an - * error for the whole set of requests. - */ -test('batch endpoint without reorderResultsByKey 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', - }, - }, - }; - - const resources = { - foo: ({ foo_ids, bar }) => { - if (_.isEqual(foo_ids, [1, 2, 3])) { - return Promise.resolve([ - { - foo_id: 1, - foo_value: 'greetings', - extra_stuff: 'lorem ipsum', - }, - // deliberately omit 2 - { - foo_id: 3, - foo_value: 'greetings', - extra_stuff: 'lorem ipsum', - }, - ]); - } else if (_.isEqual(foo_ids, [4])) { - return Promise.resolve([ - { - foo_id: 4, - foo_value: 'greetings', - }, - ]); - } - }, - }; - - await createDataLoaders(config, async (getLoaders) => { - const loaders = getLoaders(resources); - - const results = await loaders.foo.loadMany([ - { foo_id: 1, include_extra_info: true }, - { foo_id: 2, include_extra_info: true }, - { foo_id: 3, include_extra_info: true }, - { foo_id: 4, include_extra_info: false }, - ]); - - expect(results).toMatchObject([ - expect.toBeError( - '[dataloader-codegen :: foo] Resource returned 2 items, but we requested 3 items', - 'BatchItemNotFoundError', - ), - expect.toBeError( - '[dataloader-codegen :: foo] Resource returned 2 items, but we requested 3 items', - 'BatchItemNotFoundError', - ), - expect.toBeError( - '[dataloader-codegen :: foo] Resource returned 2 items, but we requested 3 items', - 'BatchItemNotFoundError', - ), - { foo_id: 4, foo_value: 'greetings' }, - ]); - }); -}); - -test('batch endpoint 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', - }, - }, - }; - - const resources = { - foo: ({ foo_ids, bar }) => { - if (_.isEqual(foo_ids, [1, 2, 3])) { - return Promise.resolve([ - { - foo_id: 1, - foo_value: 'greetings', - extra_stuff: 'lorem ipsum', - }, - // deliberately omit 2 - { - foo_id: 3, - foo_value: 'greetings', - extra_stuff: 'lorem ipsum', - }, - ]); - } else if (_.isEqual(foo_ids, [4])) { - return Promise.resolve([ - { - foo_id: 4, - foo_value: 'greetings', - extra_stuff: 'lorem ipsum', - }, - ]); - } - }, - }; - - await createDataLoaders(config, async (getLoaders) => { - const loaders = getLoaders(resources); - - const results = await loaders.foo.loadMany([ - { foo_id: 1, bar: true }, - { foo_id: 2, bar: true }, - { foo_id: 3, bar: true }, - { foo_id: 4, bar: false }, - ]); - - expect(results).toMatchObject([ - { foo_id: 1, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, - expect.toBeError( - '[dataloader-codegen :: foo] Response did not contain item with foo_id = 2', - 'BatchItemNotFoundError', - ), - { foo_id: 3, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, - { foo_id: 4, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, - ]); - }); -}); - -test('batch endpoint with isResponseDictionary handles a response that returns a dictionary', async () => { - const config = { - resources: { - foo: { - isBatchResource: true, - docsLink: 'example.com/docs/foos', - batchKey: 'foo_ids', - newKey: 'foo_id', - isResponseDictionary: true, - }, - }, - }; - - const resources = { - foo: ({ foo_ids }) => { - expect(foo_ids).toEqual([1, 2, 3]); - return Promise.resolve({ - 2: { foo_id: 2, foo_value: 'world' }, - 1: { foo_id: 1, foo_value: 'hello' }, - 3: { foo_id: 3, foo_value: '!' }, - }); - }, - }; - - await createDataLoaders(config, async (getLoaders) => { - const loaders = getLoaders(resources); - - const results = await loaders.foo.loadMany([{ foo_id: 1 }, { foo_id: 2 }, { foo_id: 3 }]); - expect(results).toEqual([ - { foo_id: 1, foo_value: 'hello' }, - { foo_id: 2, foo_value: 'world' }, - { foo_id: 3, foo_value: '!' }, - ]); - }); -}); - -test('batch endpoint with isResponseDictionary handles a response that returns a dictionary, with a missing item', async () => { - const config = { - resources: { - foo: { - isBatchResource: true, - docsLink: 'example.com/docs/foos', - batchKey: 'foo_ids', - newKey: 'foo_id', - isResponseDictionary: true, - }, - }, - }; - - const resources = { - foo: ({ foo_ids }) => { - expect(foo_ids).toEqual([1, 2, 3]); - return Promise.resolve({ - 1: { foo_id: 1, foo_value: 'hello' }, - 3: { foo_id: 3, foo_value: '!' }, - }); - }, - }; - - await createDataLoaders(config, async (getLoaders) => { - const loaders = getLoaders(resources); - - const results = await loaders.foo.loadMany([{ foo_id: 1 }, { foo_id: 2 }, { foo_id: 3 }]); - expect(results).toEqual([ - { foo_id: 1, foo_value: 'hello' }, - expect.toBeError( - '[dataloader-codegen :: foo] Could not find key = "2" in the response dict', - 'BatchItemNotFoundError', - ), - { foo_id: 3, foo_value: '!' }, - ]); - }); -}); - -test('middleware can transform the request args and the resource response', async () => { - function before(resourcePath, resourceArgs) { - expect(resourcePath).toEqual(['foo']); - expect(resourceArgs).toEqual([{ foo_ids: [100, 200, 300] }]); - - // modify the arguments to the resource - return [{ foo_ids: [1, 2, 3] }]; - } - - function after(resourcePath, response) { - expect(resourcePath).toEqual(['foo']); - expect(response).toEqual([ - { foo_id: 1, foo_value: 'hello' }, - { foo_id: 2, foo_value: 'world' }, - { foo_id: 3, foo_value: '!' }, - ]); - - return [ - { foo_id: 1, foo_value: 'goodbye' }, - { foo_id: 2, foo_value: 'world' }, - { foo_id: 3, foo_value: '?' }, - ]; - } - - const config = { - resources: { - foo: { - isBatchResource: true, - docsLink: 'example.com/docs/bar', - batchKey: 'foo_ids', - newKey: 'foo_id', - }, - }, - }; - - const resources = { - foo: ({ foo_ids }) => { - expect(foo_ids).toEqual([1, 2, 3]); - return Promise.resolve([ - { foo_id: 1, foo_value: 'hello' }, - { foo_id: 2, foo_value: 'world' }, - { foo_id: 3, foo_value: '!' }, - ]); - }, - }; - - await createDataLoaders(config, async (getLoaders) => { - const loaders = getLoaders(resources, { resourceMiddleware: { before, after } }); - - const results = await loaders.foo.loadMany([{ foo_id: 100 }, { foo_id: 200 }, { foo_id: 300 }]); - expect(results).toEqual([ - { foo_id: 1, foo_value: 'goodbye' }, - { foo_id: 2, foo_value: 'world' }, - { foo_id: 3, foo_value: '?' }, - ]); - }); -}); - -test('[isBatchResource: true] returning custom errors from error handler is supported', async () => { - class MyCustomError extends Error { - constructor(...args) { - super(...args); - this.name = this.constructor.name; - this.foo = 'bar'; - Error.captureStackTrace(this, MyCustomError); - } - } - - function errorHandler(resourcePath, error) { - expect(resourcePath).toEqual(['foo']); - expect(error.message).toBe('yikes'); - return new MyCustomError('hello from custom error object'); - } - - const config = { - resources: { - foo: { - isBatchResource: true, - docsLink: 'example.com/docs/bar', - batchKey: 'foo_ids', - newKey: 'foo_id', - }, - }, - }; - - const resources = { - foo: ({ foo_ids, include_extra_info }) => { - if (_.isEqual(foo_ids, [1, 3])) { - expect(include_extra_info).toBe(false); - throw new Error('yikes'); - } - - if (_.isEqual(foo_ids, [2, 4, 5])) { - expect(include_extra_info).toBe(true); - return Promise.resolve([ - { - foo_id: 2, - foo_value: 'greetings', - extra_stuff: 'lorem ipsum', - }, - { - foo_id: 4, - foo_value: 'greetings', - extra_stuff: 'lorem ipsum', - }, - { - foo_id: 5, - foo_value: 'greetings', - extra_stuff: 'lorem ipsum', - }, - ]); - } - }, - }; - - await createDataLoaders(config, async (getLoaders) => { - const loaders = getLoaders(resources, { errorHandler }); - - const results = await loaders.foo.loadMany([ - { foo_id: 1, include_extra_info: false }, - { foo_id: 2, include_extra_info: true }, - { foo_id: 3, include_extra_info: false }, - { foo_id: 4, include_extra_info: true }, - { foo_id: 5, include_extra_info: true }, - ]); - - expect(results).toMatchObject([ - expect.toBeError(/hello from custom error object/, 'MyCustomError'), - { foo_id: 2, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, - expect.toBeError(/hello from custom error object/, 'MyCustomError'), - { foo_id: 4, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, - { foo_id: 5, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, - ]); - - expect(results[0]).toHaveProperty('foo', 'bar'); - expect(results[2]).toHaveProperty('foo', 'bar'); - }); -}); - -test('[isBatchResource: false] returning custom errors from error handler is supported', async () => { - class MyCustomError extends Error { - constructor(...args) { - super(...args); - this.name = this.constructor.name; - this.foo = 'bar'; - Error.captureStackTrace(this, MyCustomError); - } - } - - function errorHandler(resourcePath, error) { - expect(resourcePath).toEqual(['foo']); - expect(error.message).toBe('yikes'); - return new MyCustomError('hello from custom error object'); - } - - const config = { - resources: { - foo: { - isBatchResource: false, - docsLink: 'example.com/docs/bar', - }, - }, - }; - - const resources = { - foo: ({ foo_id, include_extra_info }) => { - if ([1, 3].includes(foo_id)) { - expect(include_extra_info).toBe(false); - throw new Error('yikes'); - } - - if ([2, 4, 5].includes(foo_id)) { - expect(include_extra_info).toBe(true); - return Promise.resolve({ - foo_id, - foo_value: 'greetings', - extra_stuff: 'lorem ipsum', - }); - } - }, - }; - - await createDataLoaders(config, async (getLoaders) => { - const loaders = getLoaders(resources, { errorHandler }); - - const results = await loaders.foo.loadMany([ - { foo_id: 1, include_extra_info: false }, - { foo_id: 2, include_extra_info: true }, - { foo_id: 3, include_extra_info: false }, - { foo_id: 4, include_extra_info: true }, - { foo_id: 5, include_extra_info: true }, - ]); - - expect(results).toMatchObject([ - expect.toBeError(/hello from custom error object/, 'MyCustomError'), - { foo_id: 2, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, - expect.toBeError(/hello from custom error object/, 'MyCustomError'), - { foo_id: 4, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, - { foo_id: 5, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, - ]); - - expect(results[0]).toHaveProperty('foo', 'bar'); - expect(results[2]).toHaveProperty('foo', 'bar'); - }); -}); - -test('bail if errorHandler does not return an error', async () => { - class MyCustomError extends Error { - constructor(...args) { - super(...args); - this.name = this.constructor.name; - this.foo = 'bar'; - Error.captureStackTrace(this, MyCustomError); - } - } - - function errorHandler(resourcePath, error) { - expect(resourcePath).toEqual(['foo']); - expect(error.message).toBe('yikes'); - return 'not an Error object'; - } - - const config = { - resources: { - foo: { - isBatchResource: true, - docsLink: 'example.com/docs/bar', - batchKey: 'foo_ids', - newKey: 'foo_id', - }, - }, - }; - - const resources = { - foo: ({ foo_ids, include_extra_info }) => { - if (_.isEqual(foo_ids, [1, 3])) { - expect(include_extra_info).toBe(false); - throw new Error('yikes'); - } - - if (_.isEqual(foo_ids, [2, 4, 5])) { - expect(include_extra_info).toBe(true); - return Promise.resolve([ - { - foo_id: 2, - foo_value: 'greetings', - extra_stuff: 'lorem ipsum', - }, - { - foo_id: 4, - foo_value: 'greetings', - extra_stuff: 'lorem ipsum', - }, - { - foo_id: 5, - foo_value: 'greetings', - extra_stuff: 'lorem ipsum', - }, - ]); - } - }, - }; - - await createDataLoaders(config, async (getLoaders) => { - const loaders = getLoaders(resources, { errorHandler }); - - const results = await loaders.foo.loadMany([ - { foo_id: 1, include_extra_info: false }, - { foo_id: 2, include_extra_info: true }, - { foo_id: 3, include_extra_info: false }, - { foo_id: 4, include_extra_info: true }, - { foo_id: 5, include_extra_info: true }, - ]); - - expect(results).toMatchObject([ - expect.toBeError(/errorHandler did not return an Error object. Instead, got string: 'not an Error object'/), - { foo_id: 2, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, - expect.toBeError(/errorHandler did not return an Error object. Instead, got string: 'not an Error object'/), - { foo_id: 4, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, - { foo_id: 5, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, - ]); - }); -}); +// test('batch endpoint that rejects', async () => { +// const config = { +// resources: { +// foo: { +// isBatchResource: true, +// docsLink: 'example.com/docs/bar', +// batchKey: 'foo_ids', +// newKey: 'foo_id', +// }, +// }, +// }; + +// const resources = { +// foo: ({ foo_ids, include_extra_info }) => { +// if (_.isEqual(foo_ids, [1, 3])) { +// expect(include_extra_info).toBe(false); +// return Promise.reject('yikes'); +// } + +// if (_.isEqual(foo_ids, [2, 4, 5])) { +// expect(include_extra_info).toBe(true); +// return Promise.resolve([ +// { +// foo_id: 2, +// foo_value: 'greetings', +// extra_stuff: 'lorem ipsum', +// }, +// { +// foo_id: 4, +// foo_value: 'greetings', +// extra_stuff: 'lorem ipsum', +// }, +// { +// foo_id: 5, +// foo_value: 'greetings', +// extra_stuff: 'lorem ipsum', +// }, +// ]); +// } +// }, +// }; + +// await createDataLoaders(config, async (getLoaders) => { +// const loaders = getLoaders(resources); + +// const results = await loaders.foo.loadMany([ +// { foo_id: 1, include_extra_info: false }, +// { foo_id: 2, include_extra_info: true }, +// { foo_id: 3, include_extra_info: false }, +// { foo_id: 4, include_extra_info: true }, +// { foo_id: 5, include_extra_info: true }, +// ]); + +// // NonError comes from the default error handler which uses ensure-error +// expect(results).toMatchObject([ +// expect.toBeError(/yikes/, 'NonError'), +// { foo_id: 2, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, +// expect.toBeError(/yikes/, 'NonError'), +// { foo_id: 4, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, +// { foo_id: 5, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, +// ]); +// }); +// }); + +// test('batch endpoint (multiple requests, default error handling)', async () => { +// const config = { +// resources: { +// foo: { +// isBatchResource: true, +// docsLink: 'example.com/docs/bar', +// batchKey: 'foo_ids', +// newKey: 'foo_id', +// }, +// }, +// }; + +// const resources = { +// foo: ({ foo_ids, include_extra_info }) => { +// if (_.isEqual(foo_ids, [1, 3])) { +// expect(include_extra_info).toBe(false); +// throw new Error('yikes'); +// } + +// if (_.isEqual(foo_ids, [2, 4, 5])) { +// expect(include_extra_info).toBe(true); +// return Promise.resolve([ +// { +// foo_id: 2, +// foo_value: 'greetings', +// extra_stuff: 'lorem ipsum', +// }, +// { +// foo_id: 4, +// foo_value: 'greetings', +// extra_stuff: 'lorem ipsum', +// }, +// { +// foo_id: 5, +// foo_value: 'greetings', +// extra_stuff: 'lorem ipsum', +// }, +// ]); +// } +// }, +// }; + +// await createDataLoaders(config, async (getLoaders) => { +// const loaders = getLoaders(resources); + +// const results = await loaders.foo.loadMany([ +// { foo_id: 1, include_extra_info: false }, +// { foo_id: 2, include_extra_info: true }, +// { foo_id: 3, include_extra_info: false }, +// { foo_id: 4, include_extra_info: true }, +// { foo_id: 5, include_extra_info: true }, +// ]); + +// expect(results).toMatchObject([ +// expect.toBeError(/yikes/), +// { foo_id: 2, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, +// expect.toBeError(/yikes/), +// { foo_id: 4, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, +// { foo_id: 5, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, +// ]); +// }); +// }); + +// test('batch endpoint (multiple requests, custom error handling)', async () => { +// async function errorHandler(resourcePath, error) { +// expect(resourcePath).toEqual(['foo']); +// expect(error.message).toBe('yikes'); +// return new Error('hello from custom error handler'); +// } + +// const config = { +// resources: { +// foo: { +// isBatchResource: true, +// docsLink: 'example.com/docs/bar', +// batchKey: 'foo_ids', +// newKey: 'foo_id', +// }, +// }, +// }; + +// const resources = { +// foo: ({ foo_ids, include_extra_info }) => { +// if (_.isEqual(foo_ids, [1, 3])) { +// expect(include_extra_info).toBe(false); +// throw new Error('yikes'); +// } + +// if (_.isEqual(foo_ids, [2, 4, 5])) { +// expect(include_extra_info).toBe(true); +// return Promise.resolve([ +// { +// foo_id: 2, +// foo_value: 'greetings', +// extra_stuff: 'lorem ipsum', +// }, +// { +// foo_id: 4, +// foo_value: 'greetings', +// extra_stuff: 'lorem ipsum', +// }, +// { +// foo_id: 5, +// foo_value: 'greetings', +// extra_stuff: 'lorem ipsum', +// }, +// ]); +// } +// }, +// }; + +// await createDataLoaders(config, async (getLoaders) => { +// const loaders = getLoaders(resources, { errorHandler }); + +// const results = await loaders.foo.loadMany([ +// { foo_id: 1, include_extra_info: false }, +// { foo_id: 2, include_extra_info: true }, +// { foo_id: 3, include_extra_info: false }, +// { foo_id: 4, include_extra_info: true }, +// { foo_id: 5, include_extra_info: true }, +// ]); + +// expect(results).toMatchObject([ +// expect.toBeError(/hello from custom error handler/), +// { foo_id: 2, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, +// expect.toBeError(/hello from custom error handler/), +// { foo_id: 4, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, +// { foo_id: 5, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, +// ]); +// }); +// }); + +// test('batch endpoint (multiple requests, error handling, nestedPath)', async () => { +// const config = { +// eek: true, +// resources: { +// foo: { +// isBatchResource: true, +// docsLink: 'example.com/docs/bar', +// batchKey: 'foo_ids', +// newKey: 'foo_id', +// nestedPath: 'foo_data', +// }, +// }, +// }; + +// const resources = { +// foo: ({ foo_ids, include_extra_info }) => { +// if (_.isEqual(foo_ids, [1, 3])) { +// expect(include_extra_info).toBe(false); +// return new Error('yikes'); +// } + +// if (_.isEqual(foo_ids, [2, 4, 5])) { +// expect(include_extra_info).toBe(true); +// return Promise.resolve({ +// foo_data: [ +// { +// foo_id: 2, +// foo_value: 'greetings', +// extra_stuff: 'lorem ipsum', +// }, +// { +// foo_id: 4, +// foo_value: 'greetings', +// extra_stuff: 'lorem ipsum', +// }, +// { +// foo_id: 5, +// foo_value: 'greetings', +// extra_stuff: 'lorem ipsum', +// }, +// ], +// }); +// } +// }, +// }; + +// await createDataLoaders(config, async (getLoaders) => { +// const loaders = getLoaders(resources); + +// const results = await loaders.foo.loadMany([ +// { foo_id: 1, include_extra_info: false }, +// { foo_id: 2, include_extra_info: true }, +// { foo_id: 3, include_extra_info: false }, +// { foo_id: 4, include_extra_info: true }, +// { foo_id: 5, include_extra_info: true }, +// ]); + +// expect(results).toMatchObject([ +// expect.toBeError(/yikes/), +// { foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, +// expect.toBeError(/yikes/), +// { foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, +// { foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, +// ]); +// }); +// }); + +// test('batch endpoint (multiple requests, error handling - non array response)', async () => { +// const config = { +// eek: true, +// resources: { +// foo: { +// isBatchResource: true, +// docsLink: 'example.com/docs/bar', +// batchKey: 'foo_ids', +// newKey: 'foo_id', +// }, +// }, +// }; + +// const resources = { +// foo: ({ foo_ids, include_extra_info }) => { +// if (_.isEqual(foo_ids, [1, 3])) { +// expect(include_extra_info).toBe(false); +// return new Error('yikes'); +// } + +// if (_.isEqual(foo_ids, [2, 4, 5])) { +// expect(include_extra_info).toBe(true); +// // Deliberately returning an object, not an array +// return Promise.resolve({ +// foo: 'bar', +// }); +// } +// }, +// }; + +// await createDataLoaders(config, async (getLoaders) => { +// const loaders = getLoaders(resources); + +// const results = await loaders.foo.loadMany([ +// { foo_id: 1, include_extra_info: false }, +// { foo_id: 2, include_extra_info: true }, +// { foo_id: 3, include_extra_info: false }, +// { foo_id: 4, include_extra_info: true }, +// { foo_id: 5, include_extra_info: true }, +// ]); + +// expect(results).toMatchObject([ +// expect.toBeError('yikes'), +// expect.toBeError('[dataloader-codegen :: foo] Expected response to be an array'), +// expect.toBeError('yikes'), +// expect.toBeError('[dataloader-codegen :: foo] Expected response to be an array'), +// expect.toBeError('[dataloader-codegen :: foo] Expected response to be an array'), +// ]); +// }); +// }); + +// test('batch endpoint (multiple requests, error handling, with reordering)', async () => { +// const config = { +// resources: { +// foo: { +// isBatchResource: true, +// docsLink: 'example.com/docs/bar', +// batchKey: 'foo_ids', +// newKey: 'foo_id', +// reorderResultsByKey: 'foo_id', +// }, +// }, +// }; + +// const resources = { +// foo: ({ foo_ids, include_extra_info }) => { +// if (_.isEqual(foo_ids, [1, 3])) { +// expect(include_extra_info).toBe(false); +// return new Error('yikes'); +// } + +// if (_.isEqual(foo_ids, [2, 4, 5])) { +// expect(include_extra_info).toBe(true); +// // return items deliberately out of order +// return Promise.resolve([ +// { +// foo_id: 4, +// foo_value: 'greetings', +// extra_stuff: 'lorem ipsum', +// }, +// { +// foo_id: 5, +// foo_value: 'greetings', +// extra_stuff: 'lorem ipsum', +// }, +// { +// foo_id: 2, +// foo_value: 'greetings', +// extra_stuff: 'lorem ipsum', +// }, +// ]); +// } +// }, +// }; + +// await createDataLoaders(config, async (getLoaders) => { +// const loaders = getLoaders(resources); + +// const results = await loaders.foo.loadMany([ +// { foo_id: 1, include_extra_info: false }, +// { foo_id: 2, include_extra_info: true }, +// { foo_id: 3, include_extra_info: false }, +// { foo_id: 4, include_extra_info: true }, +// { foo_id: 5, include_extra_info: true }, +// ]); + +// expect(results).toMatchObject([ +// expect.toBeError(/yikes/), +// { foo_id: 2, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, +// expect.toBeError(/yikes/), +// { foo_id: 4, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, +// { foo_id: 5, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, +// ]); +// }); +// }); + +// /** +// * Without reorderResultsByKey: +// * If we requested 3 items, but the resource only returns 2, we don't know which +// * response is missing. It's unsafe to return any results, so we must throw an +// * error for the whole set of requests. +// */ +// test('batch endpoint without reorderResultsByKey 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', +// }, +// }, +// }; + +// const resources = { +// foo: ({ foo_ids, bar }) => { +// if (_.isEqual(foo_ids, [1, 2, 3])) { +// return Promise.resolve([ +// { +// foo_id: 1, +// foo_value: 'greetings', +// extra_stuff: 'lorem ipsum', +// }, +// // deliberately omit 2 +// { +// foo_id: 3, +// foo_value: 'greetings', +// extra_stuff: 'lorem ipsum', +// }, +// ]); +// } else if (_.isEqual(foo_ids, [4])) { +// return Promise.resolve([ +// { +// foo_id: 4, +// foo_value: 'greetings', +// }, +// ]); +// } +// }, +// }; + +// await createDataLoaders(config, async (getLoaders) => { +// const loaders = getLoaders(resources); + +// const results = await loaders.foo.loadMany([ +// { foo_id: 1, include_extra_info: true }, +// { foo_id: 2, include_extra_info: true }, +// { foo_id: 3, include_extra_info: true }, +// { foo_id: 4, include_extra_info: false }, +// ]); + +// expect(results).toMatchObject([ +// expect.toBeError( +// '[dataloader-codegen :: foo] Resource returned 2 items, but we requested 3 items', +// 'BatchItemNotFoundError', +// ), +// expect.toBeError( +// '[dataloader-codegen :: foo] Resource returned 2 items, but we requested 3 items', +// 'BatchItemNotFoundError', +// ), +// expect.toBeError( +// '[dataloader-codegen :: foo] Resource returned 2 items, but we requested 3 items', +// 'BatchItemNotFoundError', +// ), +// { foo_id: 4, foo_value: 'greetings' }, +// ]); +// }); +// }); + +// test('batch endpoint 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', +// }, +// }, +// }; + +// const resources = { +// foo: ({ foo_ids, bar }) => { +// if (_.isEqual(foo_ids, [1, 2, 3])) { +// return Promise.resolve([ +// { +// foo_id: 1, +// foo_value: 'greetings', +// extra_stuff: 'lorem ipsum', +// }, +// // deliberately omit 2 +// { +// foo_id: 3, +// foo_value: 'greetings', +// extra_stuff: 'lorem ipsum', +// }, +// ]); +// } else if (_.isEqual(foo_ids, [4])) { +// return Promise.resolve([ +// { +// foo_id: 4, +// foo_value: 'greetings', +// extra_stuff: 'lorem ipsum', +// }, +// ]); +// } +// }, +// }; + +// await createDataLoaders(config, async (getLoaders) => { +// const loaders = getLoaders(resources); + +// const results = await loaders.foo.loadMany([ +// { foo_id: 1, bar: true }, +// { foo_id: 2, bar: true }, +// { foo_id: 3, bar: true }, +// { foo_id: 4, bar: false }, +// ]); + +// expect(results).toMatchObject([ +// { foo_id: 1, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, +// expect.toBeError( +// '[dataloader-codegen :: foo] Response did not contain item with foo_id = 2', +// 'BatchItemNotFoundError', +// ), +// { foo_id: 3, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, +// { foo_id: 4, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, +// ]); +// }); +// }); + +// test('batch endpoint with isResponseDictionary handles a response that returns a dictionary', async () => { +// const config = { +// resources: { +// foo: { +// isBatchResource: true, +// docsLink: 'example.com/docs/foos', +// batchKey: 'foo_ids', +// newKey: 'foo_id', +// isResponseDictionary: true, +// }, +// }, +// }; + +// const resources = { +// foo: ({ foo_ids }) => { +// expect(foo_ids).toEqual([1, 2, 3]); +// return Promise.resolve({ +// 2: { foo_id: 2, foo_value: 'world' }, +// 1: { foo_id: 1, foo_value: 'hello' }, +// 3: { foo_id: 3, foo_value: '!' }, +// }); +// }, +// }; + +// await createDataLoaders(config, async (getLoaders) => { +// const loaders = getLoaders(resources); + +// const results = await loaders.foo.loadMany([{ foo_id: 1 }, { foo_id: 2 }, { foo_id: 3 }]); +// expect(results).toEqual([ +// { foo_id: 1, foo_value: 'hello' }, +// { foo_id: 2, foo_value: 'world' }, +// { foo_id: 3, foo_value: '!' }, +// ]); +// }); +// }); + +// test('batch endpoint with isResponseDictionary handles a response that returns a dictionary, with a missing item', async () => { +// const config = { +// resources: { +// foo: { +// isBatchResource: true, +// docsLink: 'example.com/docs/foos', +// batchKey: 'foo_ids', +// newKey: 'foo_id', +// isResponseDictionary: true, +// }, +// }, +// }; + +// const resources = { +// foo: ({ foo_ids }) => { +// expect(foo_ids).toEqual([1, 2, 3]); +// return Promise.resolve({ +// 1: { foo_id: 1, foo_value: 'hello' }, +// 3: { foo_id: 3, foo_value: '!' }, +// }); +// }, +// }; + +// await createDataLoaders(config, async (getLoaders) => { +// const loaders = getLoaders(resources); + +// const results = await loaders.foo.loadMany([{ foo_id: 1 }, { foo_id: 2 }, { foo_id: 3 }]); +// expect(results).toEqual([ +// { foo_id: 1, foo_value: 'hello' }, +// expect.toBeError( +// '[dataloader-codegen :: foo] Could not find key = "2" in the response dict', +// 'BatchItemNotFoundError', +// ), +// { foo_id: 3, foo_value: '!' }, +// ]); +// }); +// }); + +// test('middleware can transform the request args and the resource response', async () => { +// function before(resourcePath, resourceArgs) { +// expect(resourcePath).toEqual(['foo']); +// expect(resourceArgs).toEqual([{ foo_ids: [100, 200, 300] }]); + +// // modify the arguments to the resource +// return [{ foo_ids: [1, 2, 3] }]; +// } + +// function after(resourcePath, response) { +// expect(resourcePath).toEqual(['foo']); +// expect(response).toEqual([ +// { foo_id: 1, foo_value: 'hello' }, +// { foo_id: 2, foo_value: 'world' }, +// { foo_id: 3, foo_value: '!' }, +// ]); + +// return [ +// { foo_id: 1, foo_value: 'goodbye' }, +// { foo_id: 2, foo_value: 'world' }, +// { foo_id: 3, foo_value: '?' }, +// ]; +// } + +// const config = { +// resources: { +// foo: { +// isBatchResource: true, +// docsLink: 'example.com/docs/bar', +// batchKey: 'foo_ids', +// newKey: 'foo_id', +// }, +// }, +// }; + +// const resources = { +// foo: ({ foo_ids }) => { +// expect(foo_ids).toEqual([1, 2, 3]); +// return Promise.resolve([ +// { foo_id: 1, foo_value: 'hello' }, +// { foo_id: 2, foo_value: 'world' }, +// { foo_id: 3, foo_value: '!' }, +// ]); +// }, +// }; + +// await createDataLoaders(config, async (getLoaders) => { +// const loaders = getLoaders(resources, { resourceMiddleware: { before, after } }); + +// const results = await loaders.foo.loadMany([{ foo_id: 100 }, { foo_id: 200 }, { foo_id: 300 }]); +// expect(results).toEqual([ +// { foo_id: 1, foo_value: 'goodbye' }, +// { foo_id: 2, foo_value: 'world' }, +// { foo_id: 3, foo_value: '?' }, +// ]); +// }); +// }); + +// test('[isBatchResource: true] returning custom errors from error handler is supported', async () => { +// class MyCustomError extends Error { +// constructor(...args) { +// super(...args); +// this.name = this.constructor.name; +// this.foo = 'bar'; +// Error.captureStackTrace(this, MyCustomError); +// } +// } + +// function errorHandler(resourcePath, error) { +// expect(resourcePath).toEqual(['foo']); +// expect(error.message).toBe('yikes'); +// return new MyCustomError('hello from custom error object'); +// } + +// const config = { +// resources: { +// foo: { +// isBatchResource: true, +// docsLink: 'example.com/docs/bar', +// batchKey: 'foo_ids', +// newKey: 'foo_id', +// }, +// }, +// }; + +// const resources = { +// foo: ({ foo_ids, include_extra_info }) => { +// if (_.isEqual(foo_ids, [1, 3])) { +// expect(include_extra_info).toBe(false); +// throw new Error('yikes'); +// } + +// if (_.isEqual(foo_ids, [2, 4, 5])) { +// expect(include_extra_info).toBe(true); +// return Promise.resolve([ +// { +// foo_id: 2, +// foo_value: 'greetings', +// extra_stuff: 'lorem ipsum', +// }, +// { +// foo_id: 4, +// foo_value: 'greetings', +// extra_stuff: 'lorem ipsum', +// }, +// { +// foo_id: 5, +// foo_value: 'greetings', +// extra_stuff: 'lorem ipsum', +// }, +// ]); +// } +// }, +// }; + +// await createDataLoaders(config, async (getLoaders) => { +// const loaders = getLoaders(resources, { errorHandler }); + +// const results = await loaders.foo.loadMany([ +// { foo_id: 1, include_extra_info: false }, +// { foo_id: 2, include_extra_info: true }, +// { foo_id: 3, include_extra_info: false }, +// { foo_id: 4, include_extra_info: true }, +// { foo_id: 5, include_extra_info: true }, +// ]); + +// expect(results).toMatchObject([ +// expect.toBeError(/hello from custom error object/, 'MyCustomError'), +// { foo_id: 2, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, +// expect.toBeError(/hello from custom error object/, 'MyCustomError'), +// { foo_id: 4, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, +// { foo_id: 5, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, +// ]); + +// expect(results[0]).toHaveProperty('foo', 'bar'); +// expect(results[2]).toHaveProperty('foo', 'bar'); +// }); +// }); + +// test('[isBatchResource: false] returning custom errors from error handler is supported', async () => { +// class MyCustomError extends Error { +// constructor(...args) { +// super(...args); +// this.name = this.constructor.name; +// this.foo = 'bar'; +// Error.captureStackTrace(this, MyCustomError); +// } +// } + +// function errorHandler(resourcePath, error) { +// expect(resourcePath).toEqual(['foo']); +// expect(error.message).toBe('yikes'); +// return new MyCustomError('hello from custom error object'); +// } + +// const config = { +// resources: { +// foo: { +// isBatchResource: false, +// docsLink: 'example.com/docs/bar', +// }, +// }, +// }; + +// const resources = { +// foo: ({ foo_id, include_extra_info }) => { +// if ([1, 3].includes(foo_id)) { +// expect(include_extra_info).toBe(false); +// throw new Error('yikes'); +// } + +// if ([2, 4, 5].includes(foo_id)) { +// expect(include_extra_info).toBe(true); +// return Promise.resolve({ +// foo_id, +// foo_value: 'greetings', +// extra_stuff: 'lorem ipsum', +// }); +// } +// }, +// }; + +// await createDataLoaders(config, async (getLoaders) => { +// const loaders = getLoaders(resources, { errorHandler }); + +// const results = await loaders.foo.loadMany([ +// { foo_id: 1, include_extra_info: false }, +// { foo_id: 2, include_extra_info: true }, +// { foo_id: 3, include_extra_info: false }, +// { foo_id: 4, include_extra_info: true }, +// { foo_id: 5, include_extra_info: true }, +// ]); + +// expect(results).toMatchObject([ +// expect.toBeError(/hello from custom error object/, 'MyCustomError'), +// { foo_id: 2, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, +// expect.toBeError(/hello from custom error object/, 'MyCustomError'), +// { foo_id: 4, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, +// { foo_id: 5, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, +// ]); + +// expect(results[0]).toHaveProperty('foo', 'bar'); +// expect(results[2]).toHaveProperty('foo', 'bar'); +// }); +// }); + +// test('bail if errorHandler does not return an error', async () => { +// class MyCustomError extends Error { +// constructor(...args) { +// super(...args); +// this.name = this.constructor.name; +// this.foo = 'bar'; +// Error.captureStackTrace(this, MyCustomError); +// } +// } + +// function errorHandler(resourcePath, error) { +// expect(resourcePath).toEqual(['foo']); +// expect(error.message).toBe('yikes'); +// return 'not an Error object'; +// } + +// const config = { +// resources: { +// foo: { +// isBatchResource: true, +// docsLink: 'example.com/docs/bar', +// batchKey: 'foo_ids', +// newKey: 'foo_id', +// }, +// }, +// }; + +// const resources = { +// foo: ({ foo_ids, include_extra_info }) => { +// if (_.isEqual(foo_ids, [1, 3])) { +// expect(include_extra_info).toBe(false); +// throw new Error('yikes'); +// } + +// if (_.isEqual(foo_ids, [2, 4, 5])) { +// expect(include_extra_info).toBe(true); +// return Promise.resolve([ +// { +// foo_id: 2, +// foo_value: 'greetings', +// extra_stuff: 'lorem ipsum', +// }, +// { +// foo_id: 4, +// foo_value: 'greetings', +// extra_stuff: 'lorem ipsum', +// }, +// { +// foo_id: 5, +// foo_value: 'greetings', +// extra_stuff: 'lorem ipsum', +// }, +// ]); +// } +// }, +// }; + +// await createDataLoaders(config, async (getLoaders) => { +// const loaders = getLoaders(resources, { errorHandler }); + +// const results = await loaders.foo.loadMany([ +// { foo_id: 1, include_extra_info: false }, +// { foo_id: 2, include_extra_info: true }, +// { foo_id: 3, include_extra_info: false }, +// { foo_id: 4, include_extra_info: true }, +// { foo_id: 5, include_extra_info: true }, +// ]); + +// expect(results).toMatchObject([ +// expect.toBeError(/errorHandler did not return an Error object. Instead, got string: 'not an Error object'/), +// { foo_id: 2, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, +// expect.toBeError(/errorHandler did not return an Error object. Instead, got string: 'not an Error object'/), +// { foo_id: 4, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, +// { foo_id: 5, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, +// ]); +// }); +// }); diff --git a/examples/swapi/swapi-loaders.js b/examples/swapi/swapi-loaders.js index 12e23dd..3764cb0 100644 --- a/examples/swapi/swapi-loaders.js +++ b/examples/swapi/swapi-loaders.js @@ -277,6 +277,8 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade * We'll refer to each element in the group as a "request ID". */ const requestGroups = partitionItems('planet_id', keys); + console.log('lllllll'); + console.log(requestGroups); // Map the request groups to a list of Promises - one for each request const groupedResults = await Promise.all( @@ -299,6 +301,8 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade }, ]; + console.log('fffffffff'); + console.log(resourceArgs); let response = await (async (_resourceArgs) => { // Make a re-assignable variable so flow/eslint doesn't complain let __resourceArgs = _resourceArgs; @@ -345,7 +349,8 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade return _response; })(resourceArgs); - + console.log('ggggggg'); + console.log(response); if (!(response instanceof Error)) { } @@ -551,6 +556,8 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade * We'll refer to each element in the group as a "request ID". */ const requestGroups = partitionItems('person_id', keys); + console.log('lllllll'); + console.log(requestGroups); // Map the request groups to a list of Promises - one for each request const groupedResults = await Promise.all( @@ -573,6 +580,8 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade }, ]; + console.log('fffffffff'); + console.log(resourceArgs); let response = await (async (_resourceArgs) => { // Make a re-assignable variable so flow/eslint doesn't complain let __resourceArgs = _resourceArgs; @@ -616,7 +625,8 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade return _response; })(resourceArgs); - + console.log('ggggggg'); + console.log(response); if (!(response instanceof Error)) { } @@ -822,6 +832,8 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade * We'll refer to each element in the group as a "request ID". */ const requestGroups = partitionItems('vehicle_id', keys); + console.log('lllllll'); + console.log(requestGroups); // Map the request groups to a list of Promises - one for each request const groupedResults = await Promise.all( @@ -844,6 +856,8 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade }, ]; + console.log('fffffffff'); + console.log(resourceArgs); let response = await (async (_resourceArgs) => { // Make a re-assignable variable so flow/eslint doesn't complain let __resourceArgs = _resourceArgs; @@ -890,7 +904,8 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade return _response; })(resourceArgs); - + console.log('ggggggg'); + console.log(response); if (!(response instanceof Error)) { } diff --git a/src/codegen.ts b/src/codegen.ts index 95803bf..2ba1354 100644 --- a/src/codegen.ts +++ b/src/codegen.ts @@ -62,9 +62,12 @@ export default function codegen( CaughtResourceError, defaultErrorHandler, partitionItems, + partitionItemsWithMoreKeys, + partitionItemsByBatchKey, resultsDictToList, sortByKeys, unPartitionResults, + unPartitionResultsByBatchKeyList, } from '${runtimeHelpers}'; diff --git a/src/config.ts b/src/config.ts index 965576e..c9935e0 100644 --- a/src/config.ts +++ b/src/config.ts @@ -20,6 +20,8 @@ export interface BatchResourceConfig { isBatchResource: true; batchKey: string; newKey: string; + secondBatchKey: string; + secondNewKey: string; reorderResultsByKey?: string; nestedPath?: string; commaSeparatedBatchKey?: boolean; diff --git a/src/genTypeFlow.ts b/src/genTypeFlow.ts index 6484cb3..0597ccb 100644 --- a/src/genTypeFlow.ts +++ b/src/genTypeFlow.ts @@ -40,7 +40,8 @@ export function getLoaderTypeKey(resourceConfig: ResourceConfig, resourcePath: R // TODO: We assume that the resource accepts a single dict argument. Let's // make this configurable to handle resources that use seperate arguments. const resourceArgs = getResourceArg(resourceConfig, resourcePath); - + console.log('resourceArgs'); + console.log(resourceArgs); return resourceConfig.isBatchResource ? ` {| diff --git a/src/implementation.ts b/src/implementation.ts index e844ca2..173a2ff 100644 --- a/src/implementation.ts +++ b/src/implementation.ts @@ -27,7 +27,6 @@ function getLoaderComment(resourceConfig: ResourceConfig, resourcePath: Readonly function callResource(resourceConfig: ResourceConfig, resourcePath: ReadonlyArray): string { // The reference at runtime to where the underlying resource lives const resourceReference = ['resources', ...resourcePath].join('.'); - // Call the underlying resource, wrapped with our middleware and error handling. // Uses an iife so the result variable is assignable at the callsite (for readability) return ` @@ -165,8 +164,16 @@ function getBatchLoader(resourceConfig: BatchResourceConfig, resourcePath: Reado * * We'll refer to each element in the group as a "request ID". */ - const requestGroups = partitionItems('${resourceConfig.newKey}', keys); - + const requestGroups = partitionItemsWithMoreKeys(['${resourceConfig.newKey}', '${ + resourceConfig.secondNewKey + }'], keys); + console.log('lllllll'); + console.log(requestGroups); + console.log(keys); + const groupByBatchKey = partitionItemsByBatchKey('${resourceConfig.newKey}', ['${ + resourceConfig.newKey + }', '${resourceConfig.secondNewKey}'], keys); + console.log(groupByBatchKey); // Map the request groups to a list of Promises - one for each request const groupedResults = await Promise.all(requestGroups.map(async requestIDs => { /** @@ -177,26 +184,37 @@ function getBatchLoader(resourceConfig: BatchResourceConfig, resourcePath: Reado * send to the resource as a batch request! */ const requests = requestIDs.map(id => keys[id]); + console.log('rrrrrrrrr'); + console.log(requests[0]); ${(() => { - const { batchKey, newKey, commaSeparatedBatchKey } = resourceConfig; + const { batchKey, newKey, secondBatchKey, secondNewKey, commaSeparatedBatchKey } = resourceConfig; let batchKeyParam = `['${batchKey}']: requests.map(k => k['${newKey}'])`; if (commaSeparatedBatchKey === true) { batchKeyParam = `${batchKeyParam}.join(',')`; } + let secondBatchKeyParam = `['${secondBatchKey}']: requests.map(k => k['${secondNewKey}'])`; + if (commaSeparatedBatchKey === true) { + secondBatchKeyParam = `${secondBatchKeyParam}.join(',')`; + } return ` // For now, we assume that the dataloader key should be the first argument to the resource // @see https://github.com/Yelp/dataloader-codegen/issues/56 const resourceArgs = [{ - ..._.omit(requests[0], '${resourceConfig.newKey}'), + ..._.omit(requests[0], '${resourceConfig.newKey}', '${resourceConfig.secondNewKey}'), ${batchKeyParam}, + ${secondBatchKeyParam}, }]; `; })()} - + console.log('fffffffff'); + const batchKeyList = resourceArgs[0]['${resourceConfig.batchKey}']; + console.log(batchKeyList); let response = await ${callResource(resourceConfig, resourcePath)}(resourceArgs); + console.log('ggggggg'); + console.log(response); if (!(response instanceof Error)) { ${(() => { @@ -284,22 +302,7 @@ function getBatchLoader(resourceConfig: BatchResourceConfig, resourcePath: Reado * we don't know _which_ key's response is missing. Therefore * it's unsafe to return the response array back. */ - if (response.length !== requests.length) { - /** - * We must return errors for all keys in this group :( - */ - response = new BatchItemNotFoundError([ - \`${errorPrefix( - resourcePath, - )} Resource returned \${response.length} items, but we requested \${requests.length} items.\`, - 'Add reorderResultsByKey to the config for this resource to be able to handle a partial response.', - ].join(' ')); - - // Tell flow that BatchItemNotFoundError extends Error. - // It's an issue with flowgen package, but not an issue with Flow. - // @see https://github.com/Yelp/dataloader-codegen/pull/35#discussion_r394777533 - invariant(response instanceof Error, 'expected BatchItemNotFoundError to be an Error'); - } + } `; } else { @@ -386,9 +389,12 @@ function getBatchLoader(resourceConfig: BatchResourceConfig, resourcePath: Reado return response; })) - + console.log('vvvvvvvvvv'); + console.log(groupByBatchKey); + console.log(requestGroups); + console.log(groupedResults); // Split the results back up into the order that they were requested - return unPartitionResults(requestGroups, groupedResults); + return unPartitionResultsByBatchKeyList(groupByBatchKey, requestGroups, groupedResults); }, { ${ diff --git a/src/runtimeHelpers.ts b/src/runtimeHelpers.ts index 97375ee..91ed80e 100644 --- a/src/runtimeHelpers.ts +++ b/src/runtimeHelpers.ts @@ -94,6 +94,42 @@ export function partitionItems(ignoreKey: string, items: ReadonlyArray): return Object.values(groups); } +export function partitionItemsWithMoreKeys( + ignoreKey: Array, + items: ReadonlyArray, +): ReadonlyArray> { + const groups: { + [key: string]: Array; + } = {}; + console.log('bbbbbbb'); + console.log(items); + items.forEach((item, i) => { + const hash = objectHash(_.omit(item, ignoreKey), { algorithm: 'passthrough' }); + groups[hash] = groups[hash] || []; + groups[hash].push(i); + }); + + return Object.values(groups); +} + +export function partitionItemsByBatchKey( + batchKey: string, + ignoreKey: Array, + items: ReadonlyArray, +): ReadonlyArray> { + const groups: { + [key: string]: Array; + } = {}; + items.forEach((item, i) => { + const hash = objectHash(_.omit(item, ignoreKey), { algorithm: 'passthrough' }); + groups[hash] = groups[hash] || []; + // let map = {}; + // map[i] = items[i][batchKey]; + groups[hash].push(items[i][batchKey]); + }); + + return Object.values(groups); +} /** * Utility function to sort array of objects by a list of corresponding IDs * @@ -226,7 +262,72 @@ export function unPartitionResults( * ``` */ const zippedGroups = requestGroups.map((ids, i) => ids.map((id, j) => ({ order: id, result: resultGroups[i][j] }))); + console.log('zippedGroups'); + console.log(zippedGroups); + + /** + * Flatten and sort the groups - e.g.: + * ```js + * [ + * { order: 0, result: { foo: 'foo' } }, + * { order: 1, result: { baz: 'baz' } }, + * { order: 2, result: { bar: 'bar' } } + * ] + * ``` + */ + const sortedResults: ReadonlyArray<{ order: number; result: T | Error }> = _.sortBy(_.flatten(zippedGroups), [ + 'order', + ]); + console.log('sortedResults'); + console.log(sortedResults); + + // Now that we have a sorted array, return the actual results! + return sortedResults + .map((r) => r.result) + .map((result) => { + if (result instanceof CaughtResourceError) { + console.log('nnnnn'); + console.log(result.cause); + return result.cause; + } else { + console.log('mmmmm'); + console.log(result); + return result; + } + }); +} + +export function unPartitionResultsByBatchKeyList( + groupByBatchKey, + /** Should be a nested array of IDs, as generated by partitionItems */ + requestGroups: ReadonlyArray>, + /** The results back from the service, in the same shape as groups */ + resultGroups: ReadonlyArray>, +): ReadonlyArray { + /** + * e.g. with our inputs, produce: + * ```js + * [ + * [ + * { order: 0, result: { foo: 'foo' } }, + * { order: 2, result: { bar: 'bar' } }, + * ], + * [ + * { order: 1, result: { baz: 'baz' } }, + * ] + * ] + * ``` + */ + const zippedGroups = requestGroups.map(function (ids, i) { + return ids.map(function (id, j) { + console.log('.......'); + console.log(groupByBatchKey[i][j]); + return { order: id, result: resultGroups[i][j] }; + }); + }); + console.log('zippedGroups'); + console.log(_.flatten(zippedGroups)); /** * Flatten and sort the groups - e.g.: * ```js @@ -240,14 +341,20 @@ export function unPartitionResults( const sortedResults: ReadonlyArray<{ order: number; result: T | Error }> = _.sortBy(_.flatten(zippedGroups), [ 'order', ]); + console.log('sortedResults'); + console.log(sortedResults); // Now that we have a sorted array, return the actual results! return sortedResults .map((r) => r.result) .map((result) => { if (result instanceof CaughtResourceError) { + console.log('nnnnn'); + console.log(result.cause); return result.cause; } else { + console.log('mmmmm'); + console.log(result); return result; } }); From 553dd81d4bdea0bc9546290d41076b7708d7ffcb Mon Sep 17 00:00:00 2001 From: Yue Guo Date: Fri, 7 May 2021 14:53:41 -0700 Subject: [PATCH 02/24] testing --- __tests__/implementation.test.js | 17 ++++++++--------- src/runtimeHelpers.ts | 12 +++++++++++- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/__tests__/implementation.test.js b/__tests__/implementation.test.js index c7905d1..0946344 100644 --- a/__tests__/implementation.test.js +++ b/__tests__/implementation.test.js @@ -259,8 +259,8 @@ test('batch endpoint (multiple requests)', async () => { if (_.isEqual(foo_ids, [2, 1])) { expect(include_extra_info).toBe(false); return Promise.resolve([ - { foo_id: 1, photo: 'hello', id: 'world', name: 'greetings' }, - { foo_id: 2, photo: 'hello', id: 'world', name: 'greetings' }, + { foo_id: 1, photo: 'photo1', id: 'id1', name: 'name1' }, + { foo_id: 2, photo: 'photo2', id: 'id2', name: 'name2' }, ]); } @@ -269,9 +269,9 @@ test('batch endpoint (multiple requests)', async () => { return Promise.resolve([ { foo_id: 3, - photo: 'hello', - id: 'world', - name: 'greetings', + photo: 'photo3', + id: 'id3', + name: 'name3', extra_stuff: 'lorem ipsum', }, ]); @@ -289,10 +289,9 @@ test('batch endpoint (multiple requests)', async () => { ]); expect(results).toEqual([ - { foo_id: 1, photo: 'hello' }, - - { foo_id: 3, name: 'greetings', extra_stuff: 'lorem ipsum' }, - { foo_id: 2, id: 'world' }, + { foo_id: 2, photo: 'photo2', id: 'id2', name: 'name2' }, + { foo_id: 1, photo: 'photo1', id: 'id1', name: 'name1' }, + { foo_id: 3, photo: 'photo3', id: 'id3', name: 'name3', extra_stuff: 'lorem ipsum' }, ]); }); }); diff --git a/src/runtimeHelpers.ts b/src/runtimeHelpers.ts index 91ed80e..7fdb2c4 100644 --- a/src/runtimeHelpers.ts +++ b/src/runtimeHelpers.ts @@ -322,7 +322,17 @@ export function unPartitionResultsByBatchKeyList( return ids.map(function (id, j) { console.log('.......'); console.log(groupByBatchKey[i][j]); - return { order: id, result: resultGroups[i][j] }; + for (const element of Object.values(resultGroups)[i]) { + if (groupByBatchKey[i][j] in element || Object.values(element).includes(groupByBatchKey[i][j])) { + return { order: id, result: element }; + } + + // if (element.has(groupByBatchKey[i][j])) { + // return { order: id, result: element}; + // } + } + //console.log(Object.values(resultGroups)[i].includes()); + // return { order: id, result: resultGroups[i][j] }; }); }); console.log('zippedGroups'); From 3b85fe36509b204a99e7481b6d9a06f63336f553 Mon Sep 17 00:00:00 2001 From: Yue Guo Date: Fri, 7 May 2021 15:28:59 -0700 Subject: [PATCH 03/24] second -> secondar, and remove console.log --- __tests__/implementation.test.js | 4 ++-- src/codegen.ts | 2 +- src/config.ts | 4 ++-- src/genTypeFlow.ts | 3 +-- src/implementation.ts | 35 ++++++++++++-------------------- src/runtimeHelpers.ts | 19 ----------------- 6 files changed, 19 insertions(+), 48 deletions(-) diff --git a/__tests__/implementation.test.js b/__tests__/implementation.test.js index 0946344..cd395e8 100644 --- a/__tests__/implementation.test.js +++ b/__tests__/implementation.test.js @@ -248,8 +248,8 @@ test('batch endpoint (multiple requests)', async () => { docsLink: 'example.com/docs/bar', batchKey: 'foo_ids', newKey: 'foo_id', - secondBatchKey: 'properties', - secondNewKey: 'property', + secondaryBatchKey: 'properties', + secondaryNewKey: 'property', }, }, }; diff --git a/src/codegen.ts b/src/codegen.ts index 2ba1354..60236fc 100644 --- a/src/codegen.ts +++ b/src/codegen.ts @@ -62,8 +62,8 @@ export default function codegen( CaughtResourceError, defaultErrorHandler, partitionItems, - partitionItemsWithMoreKeys, partitionItemsByBatchKey, + partitionItemsWithMoreKeys, resultsDictToList, sortByKeys, unPartitionResults, diff --git a/src/config.ts b/src/config.ts index c9935e0..8046984 100644 --- a/src/config.ts +++ b/src/config.ts @@ -20,8 +20,8 @@ export interface BatchResourceConfig { isBatchResource: true; batchKey: string; newKey: string; - secondBatchKey: string; - secondNewKey: string; + secondaryBatchKey: string; + secondaryNewKey: string; reorderResultsByKey?: string; nestedPath?: string; commaSeparatedBatchKey?: boolean; diff --git a/src/genTypeFlow.ts b/src/genTypeFlow.ts index 0597ccb..6484cb3 100644 --- a/src/genTypeFlow.ts +++ b/src/genTypeFlow.ts @@ -40,8 +40,7 @@ export function getLoaderTypeKey(resourceConfig: ResourceConfig, resourcePath: R // TODO: We assume that the resource accepts a single dict argument. Let's // make this configurable to handle resources that use seperate arguments. const resourceArgs = getResourceArg(resourceConfig, resourcePath); - console.log('resourceArgs'); - console.log(resourceArgs); + return resourceConfig.isBatchResource ? ` {| diff --git a/src/implementation.ts b/src/implementation.ts index 173a2ff..d127e32 100644 --- a/src/implementation.ts +++ b/src/implementation.ts @@ -165,15 +165,11 @@ function getBatchLoader(resourceConfig: BatchResourceConfig, resourcePath: Reado * We'll refer to each element in the group as a "request ID". */ const requestGroups = partitionItemsWithMoreKeys(['${resourceConfig.newKey}', '${ - resourceConfig.secondNewKey + resourceConfig.secondaryNewKey }'], keys); - console.log('lllllll'); - console.log(requestGroups); - console.log(keys); const groupByBatchKey = partitionItemsByBatchKey('${resourceConfig.newKey}', ['${ resourceConfig.newKey - }', '${resourceConfig.secondNewKey}'], keys); - console.log(groupByBatchKey); + }', '${resourceConfig.secondaryNewKey}'], keys); // Map the request groups to a list of Promises - one for each request const groupedResults = await Promise.all(requestGroups.map(async requestIDs => { /** @@ -184,37 +180,36 @@ function getBatchLoader(resourceConfig: BatchResourceConfig, resourcePath: Reado * send to the resource as a batch request! */ const requests = requestIDs.map(id => keys[id]); - console.log('rrrrrrrrr'); - console.log(requests[0]); ${(() => { - const { batchKey, newKey, secondBatchKey, secondNewKey, commaSeparatedBatchKey } = resourceConfig; + const { + batchKey, + newKey, + secondaryBatchKey, + secondaryNewKey, + commaSeparatedBatchKey, + } = resourceConfig; let batchKeyParam = `['${batchKey}']: requests.map(k => k['${newKey}'])`; if (commaSeparatedBatchKey === true) { batchKeyParam = `${batchKeyParam}.join(',')`; } - let secondBatchKeyParam = `['${secondBatchKey}']: requests.map(k => k['${secondNewKey}'])`; + let secondaryBatchKeyParam = `['${secondaryBatchKey}']: requests.map(k => k['${secondaryNewKey}'])`; if (commaSeparatedBatchKey === true) { - secondBatchKeyParam = `${secondBatchKeyParam}.join(',')`; + secondaryBatchKeyParam = `${secondaryBatchKeyParam}.join(',')`; } return ` // For now, we assume that the dataloader key should be the first argument to the resource // @see https://github.com/Yelp/dataloader-codegen/issues/56 const resourceArgs = [{ - ..._.omit(requests[0], '${resourceConfig.newKey}', '${resourceConfig.secondNewKey}'), + ..._.omit(requests[0], '${resourceConfig.newKey}', '${resourceConfig.secondaryNewKey}'), ${batchKeyParam}, - ${secondBatchKeyParam}, + ${secondaryBatchKeyParam}, }]; `; })()} - console.log('fffffffff'); - const batchKeyList = resourceArgs[0]['${resourceConfig.batchKey}']; - console.log(batchKeyList); let response = await ${callResource(resourceConfig, resourcePath)}(resourceArgs); - console.log('ggggggg'); - console.log(response); if (!(response instanceof Error)) { ${(() => { @@ -389,10 +384,6 @@ function getBatchLoader(resourceConfig: BatchResourceConfig, resourcePath: Reado return response; })) - console.log('vvvvvvvvvv'); - console.log(groupByBatchKey); - console.log(requestGroups); - console.log(groupedResults); // Split the results back up into the order that they were requested return unPartitionResultsByBatchKeyList(groupByBatchKey, requestGroups, groupedResults); }, diff --git a/src/runtimeHelpers.ts b/src/runtimeHelpers.ts index 7fdb2c4..88b979b 100644 --- a/src/runtimeHelpers.ts +++ b/src/runtimeHelpers.ts @@ -101,8 +101,6 @@ export function partitionItemsWithMoreKeys( const groups: { [key: string]: Array; } = {}; - console.log('bbbbbbb'); - console.log(items); items.forEach((item, i) => { const hash = objectHash(_.omit(item, ignoreKey), { algorithm: 'passthrough' }); groups[hash] = groups[hash] || []; @@ -320,24 +318,13 @@ export function unPartitionResultsByBatchKeyList( */ const zippedGroups = requestGroups.map(function (ids, i) { return ids.map(function (id, j) { - console.log('.......'); - console.log(groupByBatchKey[i][j]); for (const element of Object.values(resultGroups)[i]) { if (groupByBatchKey[i][j] in element || Object.values(element).includes(groupByBatchKey[i][j])) { return { order: id, result: element }; } - - // if (element.has(groupByBatchKey[i][j])) { - // return { order: id, result: element}; - // } } - //console.log(Object.values(resultGroups)[i].includes()); - // return { order: id, result: resultGroups[i][j] }; }); }); - console.log('zippedGroups'); - - console.log(_.flatten(zippedGroups)); /** * Flatten and sort the groups - e.g.: * ```js @@ -351,20 +338,14 @@ export function unPartitionResultsByBatchKeyList( const sortedResults: ReadonlyArray<{ order: number; result: T | Error }> = _.sortBy(_.flatten(zippedGroups), [ 'order', ]); - console.log('sortedResults'); - console.log(sortedResults); // Now that we have a sorted array, return the actual results! return sortedResults .map((r) => r.result) .map((result) => { if (result instanceof CaughtResourceError) { - console.log('nnnnn'); - console.log(result.cause); return result.cause; } else { - console.log('mmmmm'); - console.log(result); return result; } }); From c283cca134d42fa8713b796cc6a18ec73ee83c08 Mon Sep 17 00:00:00 2001 From: Yue Guo Date: Fri, 7 May 2021 16:18:26 -0700 Subject: [PATCH 04/24] add secondaryBatchKey --- __tests__/implementation.test.js | 278 ++++++++++++++++++------------- src/implementation.ts | 95 ++++++++--- 2 files changed, 238 insertions(+), 135 deletions(-) diff --git a/__tests__/implementation.test.js b/__tests__/implementation.test.js index cd395e8..183888c 100644 --- a/__tests__/implementation.test.js +++ b/__tests__/implementation.test.js @@ -57,43 +57,43 @@ async function createDataLoaders(config, cb) { ); } -test('non batch endpoint', async () => { - const config = { - resources: { - foo: { - isBatchResource: false, - docsLink: 'example.com/docs/foo', - }, - }, - }; +// test('non batch endpoint', async () => { +// const config = { +// resources: { +// foo: { +// isBatchResource: false, +// docsLink: 'example.com/docs/foo', +// }, +// }, +// }; - const resources = { - foo: jest - .fn() - .mockReturnValueOnce( - Promise.resolve({ - message: 'knock knock', - message_suffix: '!', - }), - ) - .mockReturnValueOnce( - Promise.resolve({ - message: "who's there", - message_suffix: '?', - }), - ), - }; +// const resources = { +// foo: jest +// .fn() +// .mockReturnValueOnce( +// Promise.resolve({ +// message: 'knock knock', +// message_suffix: '!', +// }), +// ) +// .mockReturnValueOnce( +// Promise.resolve({ +// message: "who's there", +// message_suffix: '?', +// }), +// ), +// }; - await createDataLoaders(config, async (getLoaders) => { - const loaders = getLoaders(resources); +// await createDataLoaders(config, async (getLoaders) => { +// const loaders = getLoaders(resources); - const results = await loaders.foo.loadMany([{ bar_id: 1 }, { bar_id: 2 }]); - expect(results).toEqual([ - { message: 'knock knock', message_suffix: '!' }, - { message: "who's there", message_suffix: '?' }, - ]); - }); -}); +// const results = await loaders.foo.loadMany([{ bar_id: 1 }, { bar_id: 2 }]); +// expect(results).toEqual([ +// { message: 'knock knock', message_suffix: '!' }, +// { message: "who's there", message_suffix: '?' }, +// ]); +// }); +// }); // test('batch endpoint', async () => { // const config = { @@ -240,63 +240,59 @@ test('non batch endpoint', async () => { // }); // }); -test('batch endpoint (multiple requests)', async () => { - const config = { - resources: { - foo: { - isBatchResource: true, - docsLink: 'example.com/docs/bar', - batchKey: 'foo_ids', - newKey: 'foo_id', - secondaryBatchKey: 'properties', - secondaryNewKey: 'property', - }, - }, - }; +// test('batch endpoint (multiple requests)', async () => { +// const config = { +// resources: { +// foo: { +// isBatchResource: true, +// docsLink: 'example.com/docs/bar', +// batchKey: 'foo_ids', +// newKey: 'foo_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([ - { foo_id: 1, photo: 'photo1', id: 'id1', name: 'name1' }, - { foo_id: 2, photo: 'photo2', id: 'id2', name: 'name2' }, - ]); - } +// const resources = { +// foo: ({ foo_ids, include_extra_info }) => { +// if (_.isEqual(foo_ids, [1, 2])) { +// expect(include_extra_info).toBe(false); +// return Promise.resolve([ +// { foo_id: 1, foo_value: 'hello' }, +// { foo_id: 2, foo_value: 'world' }, +// ]); +// } - if (_.isEqual(foo_ids, [3])) { - expect(include_extra_info).toBe(true); - return Promise.resolve([ - { - foo_id: 3, - photo: 'photo3', - id: 'id3', - name: 'name3', - extra_stuff: 'lorem ipsum', - }, - ]); - } - }, - }; +// if (_.isEqual(foo_ids, [3])) { +// expect(include_extra_info).toBe(true); +// return Promise.resolve([ +// { +// foo_id: 3, +// foo_value: 'greetings', +// extra_stuff: 'lorem ipsum', +// }, +// ]); +// } +// }, +// }; - await createDataLoaders(config, async (getLoaders) => { - const loaders = getLoaders(resources); +// await createDataLoaders(config, async (getLoaders) => { +// const loaders = getLoaders(resources); - const results = await loaders.foo.loadMany([ - { foo_id: 2, property: 'name', include_extra_info: false }, - { foo_id: 1, property: 'photo', include_extra_info: false }, - { foo_id: 3, property: 'photo', include_extra_info: true }, - ]); +// const results = await loaders.foo.loadMany([ +// { foo_id: 1, include_extra_info: false }, +// { foo_id: 2, include_extra_info: false }, +// { foo_id: 3, include_extra_info: true }, +// ]); - expect(results).toEqual([ - { foo_id: 2, photo: 'photo2', id: 'id2', name: 'name2' }, - { foo_id: 1, photo: 'photo1', id: 'id1', name: 'name1' }, - { foo_id: 3, photo: 'photo3', id: 'id3', name: 'name3', extra_stuff: 'lorem ipsum' }, - ]); - }); -}); +// expect(results).toEqual([ +// { foo_id: 1, foo_value: 'hello' }, +// { foo_id: 2, foo_value: 'world' }, +// { foo_id: 3, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, +// ]); +// }); +// }); -// test('batch endpoint that rejects', async () => { +// test('batch endpoint (multiple requests) with secondaryBatchKey', async () => { // const config = { // resources: { // foo: { @@ -304,33 +300,30 @@ test('batch endpoint (multiple requests)', async () => { // docsLink: 'example.com/docs/bar', // batchKey: 'foo_ids', // newKey: 'foo_id', +// secondaryBatchKey: 'properties', +// secondaryNewKey: 'property', // }, // }, // }; // const resources = { -// foo: ({ foo_ids, include_extra_info }) => { -// if (_.isEqual(foo_ids, [1, 3])) { +// foo: ({ foo_ids, properties, include_extra_info }) => { +// if (_.isEqual(foo_ids, [2, 1])) { // expect(include_extra_info).toBe(false); -// return Promise.reject('yikes'); +// return Promise.resolve([ +// { foo_id: 1, photo: 'photo1', id: 'id1', name: 'name1' }, +// { foo_id: 2, photo: 'photo2', id: 'id2', name: 'name2' }, +// ]); // } -// if (_.isEqual(foo_ids, [2, 4, 5])) { +// if (_.isEqual(foo_ids, [3])) { // expect(include_extra_info).toBe(true); // return Promise.resolve([ // { -// foo_id: 2, -// foo_value: 'greetings', -// extra_stuff: 'lorem ipsum', -// }, -// { -// foo_id: 4, -// foo_value: 'greetings', -// extra_stuff: 'lorem ipsum', -// }, -// { -// foo_id: 5, -// foo_value: 'greetings', +// foo_id: 3, +// photo: 'photo3', +// id: 'id3', +// name: 'name3', // extra_stuff: 'lorem ipsum', // }, // ]); @@ -342,24 +335,83 @@ test('batch endpoint (multiple requests)', async () => { // const loaders = getLoaders(resources); // const results = await loaders.foo.loadMany([ -// { foo_id: 1, include_extra_info: false }, -// { foo_id: 2, include_extra_info: true }, -// { foo_id: 3, include_extra_info: false }, -// { foo_id: 4, include_extra_info: true }, -// { foo_id: 5, include_extra_info: true }, +// { foo_id: 2, property: 'name', include_extra_info: false }, +// { foo_id: 1, property: 'photo', include_extra_info: false }, +// { foo_id: 3, property: 'photo', include_extra_info: true }, // ]); -// // NonError comes from the default error handler which uses ensure-error -// expect(results).toMatchObject([ -// expect.toBeError(/yikes/, 'NonError'), -// { foo_id: 2, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, -// expect.toBeError(/yikes/, 'NonError'), -// { foo_id: 4, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, -// { foo_id: 5, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, +// expect(results).toEqual([ +// { foo_id: 2, photo: 'photo2', id: 'id2', name: 'name2' }, +// { foo_id: 1, photo: 'photo1', id: 'id1', name: 'name1' }, +// { foo_id: 3, photo: 'photo3', id: 'id3', name: 'name3', extra_stuff: 'lorem ipsum' }, // ]); // }); // }); +test('batch endpoint that rejects', async () => { + const config = { + resources: { + foo: { + isBatchResource: true, + docsLink: 'example.com/docs/bar', + batchKey: 'foo_ids', + newKey: 'foo_id', + }, + }, + }; + + const resources = { + foo: ({ foo_ids, include_extra_info }) => { + if (_.isEqual(foo_ids, [1, 3])) { + expect(include_extra_info).toBe(false); + return Promise.reject('yikes'); + } + + if (_.isEqual(foo_ids, [2, 4, 5])) { + expect(include_extra_info).toBe(true); + return Promise.resolve([ + { + foo_id: 2, + foo_value: 'greetings', + extra_stuff: 'lorem ipsum', + }, + { + foo_id: 4, + foo_value: 'greetings', + extra_stuff: 'lorem ipsum', + }, + { + foo_id: 5, + foo_value: 'greetings', + extra_stuff: 'lorem ipsum', + }, + ]); + } + }, + }; + + await createDataLoaders(config, async (getLoaders) => { + const loaders = getLoaders(resources); + + const results = await loaders.foo.loadMany([ + { foo_id: 1, include_extra_info: false }, + { foo_id: 2, include_extra_info: true }, + { foo_id: 3, include_extra_info: false }, + { foo_id: 4, include_extra_info: true }, + { foo_id: 5, include_extra_info: true }, + ]); + + // NonError comes from the default error handler which uses ensure-error + expect(results).toMatchObject([ + expect.toBeError(/yikes/, 'NonError'), + { foo_id: 2, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, + expect.toBeError(/yikes/, 'NonError'), + { foo_id: 4, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, + { foo_id: 5, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, + ]); + }); +}); + // test('batch endpoint (multiple requests, default error handling)', async () => { // const config = { // resources: { diff --git a/src/implementation.ts b/src/implementation.ts index d127e32..89d618f 100644 --- a/src/implementation.ts +++ b/src/implementation.ts @@ -27,6 +27,7 @@ function getLoaderComment(resourceConfig: ResourceConfig, resourcePath: Readonly function callResource(resourceConfig: ResourceConfig, resourcePath: ReadonlyArray): string { // The reference at runtime to where the underlying resource lives const resourceReference = ['resources', ...resourcePath].join('.'); + // Call the underlying resource, wrapped with our middleware and error handling. // Uses an iife so the result variable is assignable at the callsite (for readability) return ` @@ -164,12 +165,15 @@ function getBatchLoader(resourceConfig: BatchResourceConfig, resourcePath: Reado * * We'll refer to each element in the group as a "request ID". */ - const requestGroups = partitionItemsWithMoreKeys(['${resourceConfig.newKey}', '${ + let requestGroups; + if ('${resourceConfig.secondaryNewKey}' && '${resourceConfig.secondaryBatchKey}') { + requestGroups = partitionItemsWithMoreKeys(['${resourceConfig.newKey}', '${ resourceConfig.secondaryNewKey }'], keys); - const groupByBatchKey = partitionItemsByBatchKey('${resourceConfig.newKey}', ['${ - resourceConfig.newKey - }', '${resourceConfig.secondaryNewKey}'], keys); + } else { + requestGroups = partitionItems('${resourceConfig.newKey}', keys); + } + // Map the request groups to a list of Promises - one for each request const groupedResults = await Promise.all(requestGroups.map(async requestIDs => { /** @@ -195,22 +199,34 @@ function getBatchLoader(resourceConfig: BatchResourceConfig, resourcePath: Reado batchKeyParam = `${batchKeyParam}.join(',')`; } - let secondaryBatchKeyParam = `['${secondaryBatchKey}']: requests.map(k => k['${secondaryNewKey}'])`; - if (commaSeparatedBatchKey === true) { - secondaryBatchKeyParam = `${secondaryBatchKeyParam}.join(',')`; + if (secondaryNewKey && secondaryBatchKey) { + let secondaryBatchKeyParam = `['${secondaryBatchKey}']: requests.map(k => k['${secondaryNewKey}'])`; + if (commaSeparatedBatchKey === true) { + secondaryBatchKeyParam = `${secondaryBatchKeyParam}.join(',')`; + } + return ` + // For now, we assume that the dataloader key should be the first argument to the resource + // @see https://github.com/Yelp/dataloader-codegen/issues/56 + const resourceArgs = [{ + ..._.omit(requests[0], '${resourceConfig.newKey}', '${resourceConfig.secondaryNewKey}'), + ${batchKeyParam}, + ${secondaryBatchKeyParam}, + }]; + `; + } else { + return ` + // For now, we assume that the dataloader key should be the first argument to the resource + // @see https://github.com/Yelp/dataloader-codegen/issues/56 + const resourceArgs = [{ + ..._.omit(requests[0], '${resourceConfig.newKey}'), + ${batchKeyParam}, + }]; + `; } - return ` - // For now, we assume that the dataloader key should be the first argument to the resource - // @see https://github.com/Yelp/dataloader-codegen/issues/56 - const resourceArgs = [{ - ..._.omit(requests[0], '${resourceConfig.newKey}', '${resourceConfig.secondaryNewKey}'), - ${batchKeyParam}, - ${secondaryBatchKeyParam}, - }]; - `; })()} let response = await ${callResource(resourceConfig, resourcePath)}(resourceArgs); - + console.log('rrrrrr'); + console.log(response instanceof Error); if (!(response instanceof Error)) { ${(() => { if (typeof resourceConfig.nestedPath === 'string') { @@ -285,9 +301,18 @@ function getBatchLoader(resourceConfig: BatchResourceConfig, resourcePath: Reado } ${(() => { - const { reorderResultsByKey, isResponseDictionary } = resourceConfig; + const { + reorderResultsByKey, + isResponseDictionary, + secondaryBatchKey, + secondaryNewKey, + } = resourceConfig; - if (!isResponseDictionary && reorderResultsByKey == null) { + if ( + !isResponseDictionary && + reorderResultsByKey == null && + !(secondaryBatchKey && secondaryNewKey) + ) { return ` if (!(response instanceof Error)) { /** @@ -297,7 +322,21 @@ function getBatchLoader(resourceConfig: BatchResourceConfig, resourcePath: Reado * we don't know _which_ key's response is missing. Therefore * it's unsafe to return the response array back. */ - + if (response.length !== requests.length) { + /** + * We must return errors for all keys in this group :( + */ + response = new BatchItemNotFoundError([ + \`${errorPrefix( + resourcePath, + )} Resource returned \${response.length} items, but we requested \${requests.length} items.\`, + 'Add reorderResultsByKey to the config for this resource to be able to handle a partial response.', + ].join(' ')); + // Tell flow that BatchItemNotFoundError extends Error. + // It's an issue with flowgen package, but not an issue with Flow. + // @see https://github.com/Yelp/dataloader-codegen/pull/35#discussion_r394777533 + invariant(response instanceof Error, 'expected BatchItemNotFoundError to be an Error'); + } } `; } else { @@ -384,8 +423,20 @@ function getBatchLoader(resourceConfig: BatchResourceConfig, resourcePath: Reado return response; })) - // Split the results back up into the order that they were requested - return unPartitionResultsByBatchKeyList(groupByBatchKey, requestGroups, groupedResults); + console.log('requestGroups'); + console.log(requestGroups); + console.log('groupedResults'); + console.log(groupedResults); + if ('${resourceConfig.secondaryNewKey}' && '${resourceConfig.secondaryBatchKey}') { + const groupByBatchKey = partitionItemsByBatchKey('${resourceConfig.newKey}', ['${ + resourceConfig.newKey + }', '${resourceConfig.secondaryNewKey}'], keys); + + // Split the results back up into the order that they were requested + return unPartitionResultsByBatchKeyList(groupByBatchKey, requestGroups, groupedResults); + }else { + return unPartitionResults(requestGroups, groupedResults); + } }, { ${ From f967006e18af8fe1c1ebebc8429323c50fbea62f Mon Sep 17 00:00:00 2001 From: Yue Guo Date: Sun, 9 May 2021 20:42:58 -0700 Subject: [PATCH 05/24] add back all tests --- __tests__/implementation.test.js | 2197 +++++++++++++++--------------- src/implementation.ts | 20 +- src/runtimeHelpers.ts | 8 - 3 files changed, 1108 insertions(+), 1117 deletions(-) diff --git a/__tests__/implementation.test.js b/__tests__/implementation.test.js index 183888c..977cbc6 100644 --- a/__tests__/implementation.test.js +++ b/__tests__/implementation.test.js @@ -57,296 +57,296 @@ async function createDataLoaders(config, cb) { ); } -// test('non batch endpoint', async () => { -// const config = { -// resources: { -// foo: { -// isBatchResource: false, -// docsLink: 'example.com/docs/foo', -// }, -// }, -// }; - -// const resources = { -// foo: jest -// .fn() -// .mockReturnValueOnce( -// Promise.resolve({ -// message: 'knock knock', -// message_suffix: '!', -// }), -// ) -// .mockReturnValueOnce( -// Promise.resolve({ -// message: "who's there", -// message_suffix: '?', -// }), -// ), -// }; - -// await createDataLoaders(config, async (getLoaders) => { -// const loaders = getLoaders(resources); - -// const results = await loaders.foo.loadMany([{ bar_id: 1 }, { bar_id: 2 }]); -// expect(results).toEqual([ -// { message: 'knock knock', message_suffix: '!' }, -// { message: "who's there", message_suffix: '?' }, -// ]); -// }); -// }); - -// test('batch endpoint', async () => { -// const config = { -// resources: { -// foo: { -// isBatchResource: true, -// docsLink: 'example.com/docs/bar', -// batchKey: 'foo_ids', -// newKey: 'foo_id', -// }, -// }, -// }; - -// const resources = { -// foo: ({ foo_ids }) => { -// expect(foo_ids).toEqual([1, 2, 3]); -// return Promise.resolve([ -// { foo_id: 1, foo_value: 'hello' }, -// { foo_id: 2, foo_value: 'world' }, -// { foo_id: 3, foo_value: '!' }, -// ]); -// }, -// }; - -// await createDataLoaders(config, async (getLoaders) => { -// const loaders = getLoaders(resources); - -// const results = await loaders.foo.loadMany([{ foo_id: 1 }, { foo_id: 2 }, { foo_id: 3 }]); -// expect(results).toEqual([ -// { foo_id: 1, foo_value: 'hello' }, -// { foo_id: 2, foo_value: 'world' }, -// { foo_id: 3, foo_value: '!' }, -// ]); -// }); -// }); - -// test('batch endpoint (with reorderResultsByKey)', async () => { -// const config = { -// resources: { -// foo: { -// isBatchResource: true, -// docsLink: 'example.com/docs/bar', -// batchKey: 'foo_ids', -// newKey: 'foo_id', -// reorderResultsByKey: 'foo_id', -// }, -// }, -// }; - -// const resources = { -// foo: ({ foo_ids }) => { -// expect(foo_ids).toEqual([1, 2, 3]); -// return Promise.resolve([ -// { foo_id: 2, foo_value: 'world' }, -// { foo_id: 1, foo_value: 'hello' }, -// { foo_id: 3, foo_value: '!' }, -// ]); -// }, -// }; - -// await createDataLoaders(config, async (getLoaders) => { -// const loaders = getLoaders(resources); - -// const results = await loaders.foo.loadMany([{ foo_id: 1 }, { foo_id: 2 }, { foo_id: 3 }]); -// expect(results).toEqual([ -// { foo_id: 1, foo_value: 'hello' }, -// { foo_id: 2, foo_value: 'world' }, -// { foo_id: 3, foo_value: '!' }, -// ]); -// }); -// }); - -// test('batch endpoint (with nestedPath)', async () => { -// const config = { -// resources: { -// foo: { -// isBatchResource: true, -// docsLink: 'example.com/docs/bar', -// batchKey: 'foo_ids', -// newKey: 'foo_id', -// nestedPath: 'foos', -// }, -// }, -// }; - -// const resources = { -// foo: ({ foo_ids }) => { -// expect(foo_ids).toEqual([1, 2, 3]); -// return Promise.resolve({ -// foos: [ -// { foo_id: 1, foo_value: 'hello' }, -// { foo_id: 2, foo_value: 'world' }, -// { foo_id: 3, foo_value: '!' }, -// ], -// }); -// }, -// }; - -// await createDataLoaders(config, async (getLoaders) => { -// const loaders = getLoaders(resources); - -// const results = await loaders.foo.loadMany([{ foo_id: 1 }, { foo_id: 2 }, { foo_id: 3 }]); -// expect(results).toEqual([ -// { foo_id: 1, foo_value: 'hello' }, -// { foo_id: 2, foo_value: 'world' }, -// { foo_id: 3, foo_value: '!' }, -// ]); -// }); -// }); - -// test('batch endpoint (with commaSeparatedBatchKey)', async () => { -// const config = { -// resources: { -// foo: { -// isBatchResource: true, -// docsLink: 'example.com/docs/bar', -// batchKey: 'foo_ids', -// newKey: 'foo_id', -// commaSeparatedBatchKey: true, -// }, -// }, -// }; - -// const resources = { -// foo: ({ foo_ids }) => { -// expect(foo_ids).toEqual('1,2,3'); -// return Promise.resolve([ -// { foo_id: 1, foo_value: 'hello' }, -// { foo_id: 2, foo_value: 'world' }, -// { foo_id: 3, foo_value: '!' }, -// ]); -// }, -// }; - -// await createDataLoaders(config, async (getLoaders) => { -// const loaders = getLoaders(resources); - -// const results = await loaders.foo.loadMany([{ foo_id: 1 }, { foo_id: 2 }, { foo_id: 3 }]); -// expect(results).toEqual([ -// { foo_id: 1, foo_value: 'hello' }, -// { foo_id: 2, foo_value: 'world' }, -// { foo_id: 3, foo_value: '!' }, -// ]); -// }); -// }); - -// test('batch endpoint (multiple requests)', async () => { -// const config = { -// resources: { -// foo: { -// isBatchResource: true, -// docsLink: 'example.com/docs/bar', -// batchKey: 'foo_ids', -// newKey: 'foo_id', -// }, -// }, -// }; - -// const resources = { -// foo: ({ foo_ids, include_extra_info }) => { -// if (_.isEqual(foo_ids, [1, 2])) { -// expect(include_extra_info).toBe(false); -// return Promise.resolve([ -// { foo_id: 1, foo_value: 'hello' }, -// { foo_id: 2, foo_value: 'world' }, -// ]); -// } - -// if (_.isEqual(foo_ids, [3])) { -// expect(include_extra_info).toBe(true); -// return Promise.resolve([ -// { -// foo_id: 3, -// foo_value: 'greetings', -// extra_stuff: 'lorem ipsum', -// }, -// ]); -// } -// }, -// }; - -// await createDataLoaders(config, async (getLoaders) => { -// const loaders = getLoaders(resources); - -// const results = await loaders.foo.loadMany([ -// { foo_id: 1, include_extra_info: false }, -// { foo_id: 2, include_extra_info: false }, -// { foo_id: 3, include_extra_info: true }, -// ]); - -// expect(results).toEqual([ -// { foo_id: 1, foo_value: 'hello' }, -// { foo_id: 2, foo_value: 'world' }, -// { foo_id: 3, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, -// ]); -// }); -// }); - -// test('batch endpoint (multiple requests) with secondaryBatchKey', async () => { -// const config = { -// resources: { -// foo: { -// isBatchResource: true, -// docsLink: 'example.com/docs/bar', -// batchKey: 'foo_ids', -// newKey: 'foo_id', -// secondaryBatchKey: 'properties', -// secondaryNewKey: 'property', -// }, -// }, -// }; - -// const resources = { -// foo: ({ foo_ids, properties, include_extra_info }) => { -// if (_.isEqual(foo_ids, [2, 1])) { -// expect(include_extra_info).toBe(false); -// return Promise.resolve([ -// { foo_id: 1, photo: 'photo1', id: 'id1', name: 'name1' }, -// { foo_id: 2, photo: 'photo2', id: 'id2', name: 'name2' }, -// ]); -// } - -// if (_.isEqual(foo_ids, [3])) { -// expect(include_extra_info).toBe(true); -// return Promise.resolve([ -// { -// foo_id: 3, -// photo: 'photo3', -// id: 'id3', -// name: 'name3', -// extra_stuff: 'lorem ipsum', -// }, -// ]); -// } -// }, -// }; - -// await createDataLoaders(config, async (getLoaders) => { -// const loaders = getLoaders(resources); - -// const results = await loaders.foo.loadMany([ -// { foo_id: 2, property: 'name', include_extra_info: false }, -// { foo_id: 1, property: 'photo', include_extra_info: false }, -// { foo_id: 3, property: 'photo', include_extra_info: true }, -// ]); - -// expect(results).toEqual([ -// { foo_id: 2, photo: 'photo2', id: 'id2', name: 'name2' }, -// { foo_id: 1, photo: 'photo1', id: 'id1', name: 'name1' }, -// { foo_id: 3, photo: 'photo3', id: 'id3', name: 'name3', extra_stuff: 'lorem ipsum' }, -// ]); -// }); -// }); +test('non batch endpoint', async () => { + const config = { + resources: { + foo: { + isBatchResource: false, + docsLink: 'example.com/docs/foo', + }, + }, + }; + + const resources = { + foo: jest + .fn() + .mockReturnValueOnce( + Promise.resolve({ + message: 'knock knock', + message_suffix: '!', + }), + ) + .mockReturnValueOnce( + Promise.resolve({ + message: "who's there", + message_suffix: '?', + }), + ), + }; + + await createDataLoaders(config, async (getLoaders) => { + const loaders = getLoaders(resources); + + const results = await loaders.foo.loadMany([{ bar_id: 1 }, { bar_id: 2 }]); + expect(results).toEqual([ + { message: 'knock knock', message_suffix: '!' }, + { message: "who's there", message_suffix: '?' }, + ]); + }); +}); + +test('batch endpoint', async () => { + const config = { + resources: { + foo: { + isBatchResource: true, + docsLink: 'example.com/docs/bar', + batchKey: 'foo_ids', + newKey: 'foo_id', + }, + }, + }; + + const resources = { + foo: ({ foo_ids }) => { + expect(foo_ids).toEqual([1, 2, 3]); + return Promise.resolve([ + { foo_id: 1, foo_value: 'hello' }, + { foo_id: 2, foo_value: 'world' }, + { foo_id: 3, foo_value: '!' }, + ]); + }, + }; + + await createDataLoaders(config, async (getLoaders) => { + const loaders = getLoaders(resources); + + const results = await loaders.foo.loadMany([{ foo_id: 1 }, { foo_id: 2 }, { foo_id: 3 }]); + expect(results).toEqual([ + { foo_id: 1, foo_value: 'hello' }, + { foo_id: 2, foo_value: 'world' }, + { foo_id: 3, foo_value: '!' }, + ]); + }); +}); + +test('batch endpoint (with reorderResultsByKey)', async () => { + const config = { + resources: { + foo: { + isBatchResource: true, + docsLink: 'example.com/docs/bar', + batchKey: 'foo_ids', + newKey: 'foo_id', + reorderResultsByKey: 'foo_id', + }, + }, + }; + + const resources = { + foo: ({ foo_ids }) => { + expect(foo_ids).toEqual([1, 2, 3]); + return Promise.resolve([ + { foo_id: 2, foo_value: 'world' }, + { foo_id: 1, foo_value: 'hello' }, + { foo_id: 3, foo_value: '!' }, + ]); + }, + }; + + await createDataLoaders(config, async (getLoaders) => { + const loaders = getLoaders(resources); + + const results = await loaders.foo.loadMany([{ foo_id: 1 }, { foo_id: 2 }, { foo_id: 3 }]); + expect(results).toEqual([ + { foo_id: 1, foo_value: 'hello' }, + { foo_id: 2, foo_value: 'world' }, + { foo_id: 3, foo_value: '!' }, + ]); + }); +}); + +test('batch endpoint (with nestedPath)', async () => { + const config = { + resources: { + foo: { + isBatchResource: true, + docsLink: 'example.com/docs/bar', + batchKey: 'foo_ids', + newKey: 'foo_id', + nestedPath: 'foos', + }, + }, + }; + + const resources = { + foo: ({ foo_ids }) => { + expect(foo_ids).toEqual([1, 2, 3]); + return Promise.resolve({ + foos: [ + { foo_id: 1, foo_value: 'hello' }, + { foo_id: 2, foo_value: 'world' }, + { foo_id: 3, foo_value: '!' }, + ], + }); + }, + }; + + await createDataLoaders(config, async (getLoaders) => { + const loaders = getLoaders(resources); + + const results = await loaders.foo.loadMany([{ foo_id: 1 }, { foo_id: 2 }, { foo_id: 3 }]); + expect(results).toEqual([ + { foo_id: 1, foo_value: 'hello' }, + { foo_id: 2, foo_value: 'world' }, + { foo_id: 3, foo_value: '!' }, + ]); + }); +}); + +test('batch endpoint (with commaSeparatedBatchKey)', async () => { + const config = { + resources: { + foo: { + isBatchResource: true, + docsLink: 'example.com/docs/bar', + batchKey: 'foo_ids', + newKey: 'foo_id', + commaSeparatedBatchKey: true, + }, + }, + }; + + const resources = { + foo: ({ foo_ids }) => { + expect(foo_ids).toEqual('1,2,3'); + return Promise.resolve([ + { foo_id: 1, foo_value: 'hello' }, + { foo_id: 2, foo_value: 'world' }, + { foo_id: 3, foo_value: '!' }, + ]); + }, + }; + + await createDataLoaders(config, async (getLoaders) => { + const loaders = getLoaders(resources); + + const results = await loaders.foo.loadMany([{ foo_id: 1 }, { foo_id: 2 }, { foo_id: 3 }]); + expect(results).toEqual([ + { foo_id: 1, foo_value: 'hello' }, + { foo_id: 2, foo_value: 'world' }, + { foo_id: 3, foo_value: '!' }, + ]); + }); +}); + +test('batch endpoint (multiple requests)', async () => { + const config = { + resources: { + foo: { + isBatchResource: true, + docsLink: 'example.com/docs/bar', + batchKey: 'foo_ids', + newKey: 'foo_id', + }, + }, + }; + + const resources = { + foo: ({ foo_ids, include_extra_info }) => { + if (_.isEqual(foo_ids, [1, 2])) { + expect(include_extra_info).toBe(false); + return Promise.resolve([ + { foo_id: 1, foo_value: 'hello' }, + { foo_id: 2, foo_value: 'world' }, + ]); + } + + if (_.isEqual(foo_ids, [3])) { + expect(include_extra_info).toBe(true); + return Promise.resolve([ + { + foo_id: 3, + foo_value: 'greetings', + extra_stuff: 'lorem ipsum', + }, + ]); + } + }, + }; + + await createDataLoaders(config, async (getLoaders) => { + const loaders = getLoaders(resources); + + const results = await loaders.foo.loadMany([ + { foo_id: 1, include_extra_info: false }, + { foo_id: 2, include_extra_info: false }, + { foo_id: 3, include_extra_info: true }, + ]); + + expect(results).toEqual([ + { foo_id: 1, foo_value: 'hello' }, + { foo_id: 2, foo_value: 'world' }, + { foo_id: 3, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, + ]); + }); +}); + +test('batch endpoint (multiple requests) with secondaryBatchKey', async () => { + const config = { + resources: { + foo: { + isBatchResource: true, + docsLink: 'example.com/docs/bar', + batchKey: 'foo_ids', + newKey: 'foo_id', + secondaryBatchKey: 'properties', + secondaryNewKey: 'property', + }, + }, + }; + + const resources = { + foo: ({ foo_ids, properties, include_extra_info }) => { + if (_.isEqual(foo_ids, [2, 1])) { + expect(include_extra_info).toBe(false); + return Promise.resolve([ + { foo_id: 1, photo: 'photo1', id: 'id1', name: 'name1' }, + { foo_id: 2, photo: 'photo2', id: 'id2', name: 'name2' }, + ]); + } + + if (_.isEqual(foo_ids, [3])) { + expect(include_extra_info).toBe(true); + return Promise.resolve([ + { + foo_id: 3, + photo: 'photo3', + id: 'id3', + name: 'name3', + extra_stuff: 'lorem ipsum', + }, + ]); + } + }, + }; + + await createDataLoaders(config, async (getLoaders) => { + const loaders = getLoaders(resources); + + const results = await loaders.foo.loadMany([ + { foo_id: 2, property: 'name', include_extra_info: false }, + { foo_id: 1, property: 'photo', include_extra_info: false }, + { foo_id: 3, property: 'photo', include_extra_info: true }, + ]); + + expect(results).toEqual([ + { foo_id: 2, photo: 'photo2', id: 'id2', name: 'name2' }, + { foo_id: 1, photo: 'photo1', id: 'id1', name: 'name1' }, + { foo_id: 3, photo: 'photo3', id: 'id3', name: 'name3', extra_stuff: 'lorem ipsum' }, + ]); + }); +}); test('batch endpoint that rejects', async () => { const config = { @@ -400,7 +400,6 @@ test('batch endpoint that rejects', async () => { { foo_id: 4, include_extra_info: true }, { foo_id: 5, include_extra_info: true }, ]); - // NonError comes from the default error handler which uses ensure-error expect(results).toMatchObject([ expect.toBeError(/yikes/, 'NonError'), @@ -412,811 +411,811 @@ test('batch endpoint that rejects', async () => { }); }); -// test('batch endpoint (multiple requests, default error handling)', async () => { -// const config = { -// resources: { -// foo: { -// isBatchResource: true, -// docsLink: 'example.com/docs/bar', -// batchKey: 'foo_ids', -// newKey: 'foo_id', -// }, -// }, -// }; - -// const resources = { -// foo: ({ foo_ids, include_extra_info }) => { -// if (_.isEqual(foo_ids, [1, 3])) { -// expect(include_extra_info).toBe(false); -// throw new Error('yikes'); -// } - -// if (_.isEqual(foo_ids, [2, 4, 5])) { -// expect(include_extra_info).toBe(true); -// return Promise.resolve([ -// { -// foo_id: 2, -// foo_value: 'greetings', -// extra_stuff: 'lorem ipsum', -// }, -// { -// foo_id: 4, -// foo_value: 'greetings', -// extra_stuff: 'lorem ipsum', -// }, -// { -// foo_id: 5, -// foo_value: 'greetings', -// extra_stuff: 'lorem ipsum', -// }, -// ]); -// } -// }, -// }; - -// await createDataLoaders(config, async (getLoaders) => { -// const loaders = getLoaders(resources); - -// const results = await loaders.foo.loadMany([ -// { foo_id: 1, include_extra_info: false }, -// { foo_id: 2, include_extra_info: true }, -// { foo_id: 3, include_extra_info: false }, -// { foo_id: 4, include_extra_info: true }, -// { foo_id: 5, include_extra_info: true }, -// ]); - -// expect(results).toMatchObject([ -// expect.toBeError(/yikes/), -// { foo_id: 2, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, -// expect.toBeError(/yikes/), -// { foo_id: 4, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, -// { foo_id: 5, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, -// ]); -// }); -// }); - -// test('batch endpoint (multiple requests, custom error handling)', async () => { -// async function errorHandler(resourcePath, error) { -// expect(resourcePath).toEqual(['foo']); -// expect(error.message).toBe('yikes'); -// return new Error('hello from custom error handler'); -// } - -// const config = { -// resources: { -// foo: { -// isBatchResource: true, -// docsLink: 'example.com/docs/bar', -// batchKey: 'foo_ids', -// newKey: 'foo_id', -// }, -// }, -// }; - -// const resources = { -// foo: ({ foo_ids, include_extra_info }) => { -// if (_.isEqual(foo_ids, [1, 3])) { -// expect(include_extra_info).toBe(false); -// throw new Error('yikes'); -// } - -// if (_.isEqual(foo_ids, [2, 4, 5])) { -// expect(include_extra_info).toBe(true); -// return Promise.resolve([ -// { -// foo_id: 2, -// foo_value: 'greetings', -// extra_stuff: 'lorem ipsum', -// }, -// { -// foo_id: 4, -// foo_value: 'greetings', -// extra_stuff: 'lorem ipsum', -// }, -// { -// foo_id: 5, -// foo_value: 'greetings', -// extra_stuff: 'lorem ipsum', -// }, -// ]); -// } -// }, -// }; - -// await createDataLoaders(config, async (getLoaders) => { -// const loaders = getLoaders(resources, { errorHandler }); - -// const results = await loaders.foo.loadMany([ -// { foo_id: 1, include_extra_info: false }, -// { foo_id: 2, include_extra_info: true }, -// { foo_id: 3, include_extra_info: false }, -// { foo_id: 4, include_extra_info: true }, -// { foo_id: 5, include_extra_info: true }, -// ]); - -// expect(results).toMatchObject([ -// expect.toBeError(/hello from custom error handler/), -// { foo_id: 2, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, -// expect.toBeError(/hello from custom error handler/), -// { foo_id: 4, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, -// { foo_id: 5, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, -// ]); -// }); -// }); - -// test('batch endpoint (multiple requests, error handling, nestedPath)', async () => { -// const config = { -// eek: true, -// resources: { -// foo: { -// isBatchResource: true, -// docsLink: 'example.com/docs/bar', -// batchKey: 'foo_ids', -// newKey: 'foo_id', -// nestedPath: 'foo_data', -// }, -// }, -// }; - -// const resources = { -// foo: ({ foo_ids, include_extra_info }) => { -// if (_.isEqual(foo_ids, [1, 3])) { -// expect(include_extra_info).toBe(false); -// return new Error('yikes'); -// } - -// if (_.isEqual(foo_ids, [2, 4, 5])) { -// expect(include_extra_info).toBe(true); -// return Promise.resolve({ -// foo_data: [ -// { -// foo_id: 2, -// foo_value: 'greetings', -// extra_stuff: 'lorem ipsum', -// }, -// { -// foo_id: 4, -// foo_value: 'greetings', -// extra_stuff: 'lorem ipsum', -// }, -// { -// foo_id: 5, -// foo_value: 'greetings', -// extra_stuff: 'lorem ipsum', -// }, -// ], -// }); -// } -// }, -// }; - -// await createDataLoaders(config, async (getLoaders) => { -// const loaders = getLoaders(resources); - -// const results = await loaders.foo.loadMany([ -// { foo_id: 1, include_extra_info: false }, -// { foo_id: 2, include_extra_info: true }, -// { foo_id: 3, include_extra_info: false }, -// { foo_id: 4, include_extra_info: true }, -// { foo_id: 5, include_extra_info: true }, -// ]); - -// expect(results).toMatchObject([ -// expect.toBeError(/yikes/), -// { foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, -// expect.toBeError(/yikes/), -// { foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, -// { foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, -// ]); -// }); -// }); - -// test('batch endpoint (multiple requests, error handling - non array response)', async () => { -// const config = { -// eek: true, -// resources: { -// foo: { -// isBatchResource: true, -// docsLink: 'example.com/docs/bar', -// batchKey: 'foo_ids', -// newKey: 'foo_id', -// }, -// }, -// }; - -// const resources = { -// foo: ({ foo_ids, include_extra_info }) => { -// if (_.isEqual(foo_ids, [1, 3])) { -// expect(include_extra_info).toBe(false); -// return new Error('yikes'); -// } - -// if (_.isEqual(foo_ids, [2, 4, 5])) { -// expect(include_extra_info).toBe(true); -// // Deliberately returning an object, not an array -// return Promise.resolve({ -// foo: 'bar', -// }); -// } -// }, -// }; - -// await createDataLoaders(config, async (getLoaders) => { -// const loaders = getLoaders(resources); - -// const results = await loaders.foo.loadMany([ -// { foo_id: 1, include_extra_info: false }, -// { foo_id: 2, include_extra_info: true }, -// { foo_id: 3, include_extra_info: false }, -// { foo_id: 4, include_extra_info: true }, -// { foo_id: 5, include_extra_info: true }, -// ]); - -// expect(results).toMatchObject([ -// expect.toBeError('yikes'), -// expect.toBeError('[dataloader-codegen :: foo] Expected response to be an array'), -// expect.toBeError('yikes'), -// expect.toBeError('[dataloader-codegen :: foo] Expected response to be an array'), -// expect.toBeError('[dataloader-codegen :: foo] Expected response to be an array'), -// ]); -// }); -// }); - -// test('batch endpoint (multiple requests, error handling, with reordering)', async () => { -// const config = { -// resources: { -// foo: { -// isBatchResource: true, -// docsLink: 'example.com/docs/bar', -// batchKey: 'foo_ids', -// newKey: 'foo_id', -// reorderResultsByKey: 'foo_id', -// }, -// }, -// }; - -// const resources = { -// foo: ({ foo_ids, include_extra_info }) => { -// if (_.isEqual(foo_ids, [1, 3])) { -// expect(include_extra_info).toBe(false); -// return new Error('yikes'); -// } - -// if (_.isEqual(foo_ids, [2, 4, 5])) { -// expect(include_extra_info).toBe(true); -// // return items deliberately out of order -// return Promise.resolve([ -// { -// foo_id: 4, -// foo_value: 'greetings', -// extra_stuff: 'lorem ipsum', -// }, -// { -// foo_id: 5, -// foo_value: 'greetings', -// extra_stuff: 'lorem ipsum', -// }, -// { -// foo_id: 2, -// foo_value: 'greetings', -// extra_stuff: 'lorem ipsum', -// }, -// ]); -// } -// }, -// }; - -// await createDataLoaders(config, async (getLoaders) => { -// const loaders = getLoaders(resources); - -// const results = await loaders.foo.loadMany([ -// { foo_id: 1, include_extra_info: false }, -// { foo_id: 2, include_extra_info: true }, -// { foo_id: 3, include_extra_info: false }, -// { foo_id: 4, include_extra_info: true }, -// { foo_id: 5, include_extra_info: true }, -// ]); - -// expect(results).toMatchObject([ -// expect.toBeError(/yikes/), -// { foo_id: 2, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, -// expect.toBeError(/yikes/), -// { foo_id: 4, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, -// { foo_id: 5, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, -// ]); -// }); -// }); - -// /** -// * Without reorderResultsByKey: -// * If we requested 3 items, but the resource only returns 2, we don't know which -// * response is missing. It's unsafe to return any results, so we must throw an -// * error for the whole set of requests. -// */ -// test('batch endpoint without reorderResultsByKey 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', -// }, -// }, -// }; - -// const resources = { -// foo: ({ foo_ids, bar }) => { -// if (_.isEqual(foo_ids, [1, 2, 3])) { -// return Promise.resolve([ -// { -// foo_id: 1, -// foo_value: 'greetings', -// extra_stuff: 'lorem ipsum', -// }, -// // deliberately omit 2 -// { -// foo_id: 3, -// foo_value: 'greetings', -// extra_stuff: 'lorem ipsum', -// }, -// ]); -// } else if (_.isEqual(foo_ids, [4])) { -// return Promise.resolve([ -// { -// foo_id: 4, -// foo_value: 'greetings', -// }, -// ]); -// } -// }, -// }; - -// await createDataLoaders(config, async (getLoaders) => { -// const loaders = getLoaders(resources); - -// const results = await loaders.foo.loadMany([ -// { foo_id: 1, include_extra_info: true }, -// { foo_id: 2, include_extra_info: true }, -// { foo_id: 3, include_extra_info: true }, -// { foo_id: 4, include_extra_info: false }, -// ]); - -// expect(results).toMatchObject([ -// expect.toBeError( -// '[dataloader-codegen :: foo] Resource returned 2 items, but we requested 3 items', -// 'BatchItemNotFoundError', -// ), -// expect.toBeError( -// '[dataloader-codegen :: foo] Resource returned 2 items, but we requested 3 items', -// 'BatchItemNotFoundError', -// ), -// expect.toBeError( -// '[dataloader-codegen :: foo] Resource returned 2 items, but we requested 3 items', -// 'BatchItemNotFoundError', -// ), -// { foo_id: 4, foo_value: 'greetings' }, -// ]); -// }); -// }); - -// test('batch endpoint 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', -// }, -// }, -// }; - -// const resources = { -// foo: ({ foo_ids, bar }) => { -// if (_.isEqual(foo_ids, [1, 2, 3])) { -// return Promise.resolve([ -// { -// foo_id: 1, -// foo_value: 'greetings', -// extra_stuff: 'lorem ipsum', -// }, -// // deliberately omit 2 -// { -// foo_id: 3, -// foo_value: 'greetings', -// extra_stuff: 'lorem ipsum', -// }, -// ]); -// } else if (_.isEqual(foo_ids, [4])) { -// return Promise.resolve([ -// { -// foo_id: 4, -// foo_value: 'greetings', -// extra_stuff: 'lorem ipsum', -// }, -// ]); -// } -// }, -// }; - -// await createDataLoaders(config, async (getLoaders) => { -// const loaders = getLoaders(resources); - -// const results = await loaders.foo.loadMany([ -// { foo_id: 1, bar: true }, -// { foo_id: 2, bar: true }, -// { foo_id: 3, bar: true }, -// { foo_id: 4, bar: false }, -// ]); - -// expect(results).toMatchObject([ -// { foo_id: 1, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, -// expect.toBeError( -// '[dataloader-codegen :: foo] Response did not contain item with foo_id = 2', -// 'BatchItemNotFoundError', -// ), -// { foo_id: 3, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, -// { foo_id: 4, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, -// ]); -// }); -// }); - -// test('batch endpoint with isResponseDictionary handles a response that returns a dictionary', async () => { -// const config = { -// resources: { -// foo: { -// isBatchResource: true, -// docsLink: 'example.com/docs/foos', -// batchKey: 'foo_ids', -// newKey: 'foo_id', -// isResponseDictionary: true, -// }, -// }, -// }; - -// const resources = { -// foo: ({ foo_ids }) => { -// expect(foo_ids).toEqual([1, 2, 3]); -// return Promise.resolve({ -// 2: { foo_id: 2, foo_value: 'world' }, -// 1: { foo_id: 1, foo_value: 'hello' }, -// 3: { foo_id: 3, foo_value: '!' }, -// }); -// }, -// }; - -// await createDataLoaders(config, async (getLoaders) => { -// const loaders = getLoaders(resources); - -// const results = await loaders.foo.loadMany([{ foo_id: 1 }, { foo_id: 2 }, { foo_id: 3 }]); -// expect(results).toEqual([ -// { foo_id: 1, foo_value: 'hello' }, -// { foo_id: 2, foo_value: 'world' }, -// { foo_id: 3, foo_value: '!' }, -// ]); -// }); -// }); - -// test('batch endpoint with isResponseDictionary handles a response that returns a dictionary, with a missing item', async () => { -// const config = { -// resources: { -// foo: { -// isBatchResource: true, -// docsLink: 'example.com/docs/foos', -// batchKey: 'foo_ids', -// newKey: 'foo_id', -// isResponseDictionary: true, -// }, -// }, -// }; - -// const resources = { -// foo: ({ foo_ids }) => { -// expect(foo_ids).toEqual([1, 2, 3]); -// return Promise.resolve({ -// 1: { foo_id: 1, foo_value: 'hello' }, -// 3: { foo_id: 3, foo_value: '!' }, -// }); -// }, -// }; - -// await createDataLoaders(config, async (getLoaders) => { -// const loaders = getLoaders(resources); - -// const results = await loaders.foo.loadMany([{ foo_id: 1 }, { foo_id: 2 }, { foo_id: 3 }]); -// expect(results).toEqual([ -// { foo_id: 1, foo_value: 'hello' }, -// expect.toBeError( -// '[dataloader-codegen :: foo] Could not find key = "2" in the response dict', -// 'BatchItemNotFoundError', -// ), -// { foo_id: 3, foo_value: '!' }, -// ]); -// }); -// }); - -// test('middleware can transform the request args and the resource response', async () => { -// function before(resourcePath, resourceArgs) { -// expect(resourcePath).toEqual(['foo']); -// expect(resourceArgs).toEqual([{ foo_ids: [100, 200, 300] }]); - -// // modify the arguments to the resource -// return [{ foo_ids: [1, 2, 3] }]; -// } - -// function after(resourcePath, response) { -// expect(resourcePath).toEqual(['foo']); -// expect(response).toEqual([ -// { foo_id: 1, foo_value: 'hello' }, -// { foo_id: 2, foo_value: 'world' }, -// { foo_id: 3, foo_value: '!' }, -// ]); - -// return [ -// { foo_id: 1, foo_value: 'goodbye' }, -// { foo_id: 2, foo_value: 'world' }, -// { foo_id: 3, foo_value: '?' }, -// ]; -// } - -// const config = { -// resources: { -// foo: { -// isBatchResource: true, -// docsLink: 'example.com/docs/bar', -// batchKey: 'foo_ids', -// newKey: 'foo_id', -// }, -// }, -// }; - -// const resources = { -// foo: ({ foo_ids }) => { -// expect(foo_ids).toEqual([1, 2, 3]); -// return Promise.resolve([ -// { foo_id: 1, foo_value: 'hello' }, -// { foo_id: 2, foo_value: 'world' }, -// { foo_id: 3, foo_value: '!' }, -// ]); -// }, -// }; - -// await createDataLoaders(config, async (getLoaders) => { -// const loaders = getLoaders(resources, { resourceMiddleware: { before, after } }); - -// const results = await loaders.foo.loadMany([{ foo_id: 100 }, { foo_id: 200 }, { foo_id: 300 }]); -// expect(results).toEqual([ -// { foo_id: 1, foo_value: 'goodbye' }, -// { foo_id: 2, foo_value: 'world' }, -// { foo_id: 3, foo_value: '?' }, -// ]); -// }); -// }); - -// test('[isBatchResource: true] returning custom errors from error handler is supported', async () => { -// class MyCustomError extends Error { -// constructor(...args) { -// super(...args); -// this.name = this.constructor.name; -// this.foo = 'bar'; -// Error.captureStackTrace(this, MyCustomError); -// } -// } - -// function errorHandler(resourcePath, error) { -// expect(resourcePath).toEqual(['foo']); -// expect(error.message).toBe('yikes'); -// return new MyCustomError('hello from custom error object'); -// } - -// const config = { -// resources: { -// foo: { -// isBatchResource: true, -// docsLink: 'example.com/docs/bar', -// batchKey: 'foo_ids', -// newKey: 'foo_id', -// }, -// }, -// }; - -// const resources = { -// foo: ({ foo_ids, include_extra_info }) => { -// if (_.isEqual(foo_ids, [1, 3])) { -// expect(include_extra_info).toBe(false); -// throw new Error('yikes'); -// } - -// if (_.isEqual(foo_ids, [2, 4, 5])) { -// expect(include_extra_info).toBe(true); -// return Promise.resolve([ -// { -// foo_id: 2, -// foo_value: 'greetings', -// extra_stuff: 'lorem ipsum', -// }, -// { -// foo_id: 4, -// foo_value: 'greetings', -// extra_stuff: 'lorem ipsum', -// }, -// { -// foo_id: 5, -// foo_value: 'greetings', -// extra_stuff: 'lorem ipsum', -// }, -// ]); -// } -// }, -// }; - -// await createDataLoaders(config, async (getLoaders) => { -// const loaders = getLoaders(resources, { errorHandler }); - -// const results = await loaders.foo.loadMany([ -// { foo_id: 1, include_extra_info: false }, -// { foo_id: 2, include_extra_info: true }, -// { foo_id: 3, include_extra_info: false }, -// { foo_id: 4, include_extra_info: true }, -// { foo_id: 5, include_extra_info: true }, -// ]); - -// expect(results).toMatchObject([ -// expect.toBeError(/hello from custom error object/, 'MyCustomError'), -// { foo_id: 2, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, -// expect.toBeError(/hello from custom error object/, 'MyCustomError'), -// { foo_id: 4, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, -// { foo_id: 5, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, -// ]); - -// expect(results[0]).toHaveProperty('foo', 'bar'); -// expect(results[2]).toHaveProperty('foo', 'bar'); -// }); -// }); - -// test('[isBatchResource: false] returning custom errors from error handler is supported', async () => { -// class MyCustomError extends Error { -// constructor(...args) { -// super(...args); -// this.name = this.constructor.name; -// this.foo = 'bar'; -// Error.captureStackTrace(this, MyCustomError); -// } -// } - -// function errorHandler(resourcePath, error) { -// expect(resourcePath).toEqual(['foo']); -// expect(error.message).toBe('yikes'); -// return new MyCustomError('hello from custom error object'); -// } - -// const config = { -// resources: { -// foo: { -// isBatchResource: false, -// docsLink: 'example.com/docs/bar', -// }, -// }, -// }; - -// const resources = { -// foo: ({ foo_id, include_extra_info }) => { -// if ([1, 3].includes(foo_id)) { -// expect(include_extra_info).toBe(false); -// throw new Error('yikes'); -// } - -// if ([2, 4, 5].includes(foo_id)) { -// expect(include_extra_info).toBe(true); -// return Promise.resolve({ -// foo_id, -// foo_value: 'greetings', -// extra_stuff: 'lorem ipsum', -// }); -// } -// }, -// }; - -// await createDataLoaders(config, async (getLoaders) => { -// const loaders = getLoaders(resources, { errorHandler }); - -// const results = await loaders.foo.loadMany([ -// { foo_id: 1, include_extra_info: false }, -// { foo_id: 2, include_extra_info: true }, -// { foo_id: 3, include_extra_info: false }, -// { foo_id: 4, include_extra_info: true }, -// { foo_id: 5, include_extra_info: true }, -// ]); - -// expect(results).toMatchObject([ -// expect.toBeError(/hello from custom error object/, 'MyCustomError'), -// { foo_id: 2, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, -// expect.toBeError(/hello from custom error object/, 'MyCustomError'), -// { foo_id: 4, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, -// { foo_id: 5, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, -// ]); - -// expect(results[0]).toHaveProperty('foo', 'bar'); -// expect(results[2]).toHaveProperty('foo', 'bar'); -// }); -// }); - -// test('bail if errorHandler does not return an error', async () => { -// class MyCustomError extends Error { -// constructor(...args) { -// super(...args); -// this.name = this.constructor.name; -// this.foo = 'bar'; -// Error.captureStackTrace(this, MyCustomError); -// } -// } - -// function errorHandler(resourcePath, error) { -// expect(resourcePath).toEqual(['foo']); -// expect(error.message).toBe('yikes'); -// return 'not an Error object'; -// } - -// const config = { -// resources: { -// foo: { -// isBatchResource: true, -// docsLink: 'example.com/docs/bar', -// batchKey: 'foo_ids', -// newKey: 'foo_id', -// }, -// }, -// }; - -// const resources = { -// foo: ({ foo_ids, include_extra_info }) => { -// if (_.isEqual(foo_ids, [1, 3])) { -// expect(include_extra_info).toBe(false); -// throw new Error('yikes'); -// } - -// if (_.isEqual(foo_ids, [2, 4, 5])) { -// expect(include_extra_info).toBe(true); -// return Promise.resolve([ -// { -// foo_id: 2, -// foo_value: 'greetings', -// extra_stuff: 'lorem ipsum', -// }, -// { -// foo_id: 4, -// foo_value: 'greetings', -// extra_stuff: 'lorem ipsum', -// }, -// { -// foo_id: 5, -// foo_value: 'greetings', -// extra_stuff: 'lorem ipsum', -// }, -// ]); -// } -// }, -// }; - -// await createDataLoaders(config, async (getLoaders) => { -// const loaders = getLoaders(resources, { errorHandler }); - -// const results = await loaders.foo.loadMany([ -// { foo_id: 1, include_extra_info: false }, -// { foo_id: 2, include_extra_info: true }, -// { foo_id: 3, include_extra_info: false }, -// { foo_id: 4, include_extra_info: true }, -// { foo_id: 5, include_extra_info: true }, -// ]); - -// expect(results).toMatchObject([ -// expect.toBeError(/errorHandler did not return an Error object. Instead, got string: 'not an Error object'/), -// { foo_id: 2, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, -// expect.toBeError(/errorHandler did not return an Error object. Instead, got string: 'not an Error object'/), -// { foo_id: 4, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, -// { foo_id: 5, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, -// ]); -// }); -// }); +test('batch endpoint (multiple requests, default error handling)', async () => { + const config = { + resources: { + foo: { + isBatchResource: true, + docsLink: 'example.com/docs/bar', + batchKey: 'foo_ids', + newKey: 'foo_id', + }, + }, + }; + + const resources = { + foo: ({ foo_ids, include_extra_info }) => { + if (_.isEqual(foo_ids, [1, 3])) { + expect(include_extra_info).toBe(false); + throw new Error('yikes'); + } + + if (_.isEqual(foo_ids, [2, 4, 5])) { + expect(include_extra_info).toBe(true); + return Promise.resolve([ + { + foo_id: 2, + foo_value: 'greetings', + extra_stuff: 'lorem ipsum', + }, + { + foo_id: 4, + foo_value: 'greetings', + extra_stuff: 'lorem ipsum', + }, + { + foo_id: 5, + foo_value: 'greetings', + extra_stuff: 'lorem ipsum', + }, + ]); + } + }, + }; + + await createDataLoaders(config, async (getLoaders) => { + const loaders = getLoaders(resources); + + const results = await loaders.foo.loadMany([ + { foo_id: 1, include_extra_info: false }, + { foo_id: 2, include_extra_info: true }, + { foo_id: 3, include_extra_info: false }, + { foo_id: 4, include_extra_info: true }, + { foo_id: 5, include_extra_info: true }, + ]); + + expect(results).toMatchObject([ + expect.toBeError(/yikes/), + { foo_id: 2, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, + expect.toBeError(/yikes/), + { foo_id: 4, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, + { foo_id: 5, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, + ]); + }); +}); + +test('batch endpoint (multiple requests, custom error handling)', async () => { + async function errorHandler(resourcePath, error) { + expect(resourcePath).toEqual(['foo']); + expect(error.message).toBe('yikes'); + return new Error('hello from custom error handler'); + } + + const config = { + resources: { + foo: { + isBatchResource: true, + docsLink: 'example.com/docs/bar', + batchKey: 'foo_ids', + newKey: 'foo_id', + }, + }, + }; + + const resources = { + foo: ({ foo_ids, include_extra_info }) => { + if (_.isEqual(foo_ids, [1, 3])) { + expect(include_extra_info).toBe(false); + throw new Error('yikes'); + } + + if (_.isEqual(foo_ids, [2, 4, 5])) { + expect(include_extra_info).toBe(true); + return Promise.resolve([ + { + foo_id: 2, + foo_value: 'greetings', + extra_stuff: 'lorem ipsum', + }, + { + foo_id: 4, + foo_value: 'greetings', + extra_stuff: 'lorem ipsum', + }, + { + foo_id: 5, + foo_value: 'greetings', + extra_stuff: 'lorem ipsum', + }, + ]); + } + }, + }; + + await createDataLoaders(config, async (getLoaders) => { + const loaders = getLoaders(resources, { errorHandler }); + + const results = await loaders.foo.loadMany([ + { foo_id: 1, include_extra_info: false }, + { foo_id: 2, include_extra_info: true }, + { foo_id: 3, include_extra_info: false }, + { foo_id: 4, include_extra_info: true }, + { foo_id: 5, include_extra_info: true }, + ]); + + expect(results).toMatchObject([ + expect.toBeError(/hello from custom error handler/), + { foo_id: 2, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, + expect.toBeError(/hello from custom error handler/), + { foo_id: 4, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, + { foo_id: 5, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, + ]); + }); +}); + +test('batch endpoint (multiple requests, error handling, nestedPath)', async () => { + const config = { + eek: true, + resources: { + foo: { + isBatchResource: true, + docsLink: 'example.com/docs/bar', + batchKey: 'foo_ids', + newKey: 'foo_id', + nestedPath: 'foo_data', + }, + }, + }; + + const resources = { + foo: ({ foo_ids, include_extra_info }) => { + if (_.isEqual(foo_ids, [1, 3])) { + expect(include_extra_info).toBe(false); + return new Error('yikes'); + } + + if (_.isEqual(foo_ids, [2, 4, 5])) { + expect(include_extra_info).toBe(true); + return Promise.resolve({ + foo_data: [ + { + foo_id: 2, + foo_value: 'greetings', + extra_stuff: 'lorem ipsum', + }, + { + foo_id: 4, + foo_value: 'greetings', + extra_stuff: 'lorem ipsum', + }, + { + foo_id: 5, + foo_value: 'greetings', + extra_stuff: 'lorem ipsum', + }, + ], + }); + } + }, + }; + + await createDataLoaders(config, async (getLoaders) => { + const loaders = getLoaders(resources); + + const results = await loaders.foo.loadMany([ + { foo_id: 1, include_extra_info: false }, + { foo_id: 2, include_extra_info: true }, + { foo_id: 3, include_extra_info: false }, + { foo_id: 4, include_extra_info: true }, + { foo_id: 5, include_extra_info: true }, + ]); + + expect(results).toMatchObject([ + expect.toBeError(/yikes/), + { foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, + expect.toBeError(/yikes/), + { foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, + { foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, + ]); + }); +}); + +test('batch endpoint (multiple requests, error handling - non array response)', async () => { + const config = { + eek: true, + resources: { + foo: { + isBatchResource: true, + docsLink: 'example.com/docs/bar', + batchKey: 'foo_ids', + newKey: 'foo_id', + }, + }, + }; + + const resources = { + foo: ({ foo_ids, include_extra_info }) => { + if (_.isEqual(foo_ids, [1, 3])) { + expect(include_extra_info).toBe(false); + return new Error('yikes'); + } + + if (_.isEqual(foo_ids, [2, 4, 5])) { + expect(include_extra_info).toBe(true); + // Deliberately returning an object, not an array + return Promise.resolve({ + foo: 'bar', + }); + } + }, + }; + + await createDataLoaders(config, async (getLoaders) => { + const loaders = getLoaders(resources); + + const results = await loaders.foo.loadMany([ + { foo_id: 1, include_extra_info: false }, + { foo_id: 2, include_extra_info: true }, + { foo_id: 3, include_extra_info: false }, + { foo_id: 4, include_extra_info: true }, + { foo_id: 5, include_extra_info: true }, + ]); + + expect(results).toMatchObject([ + expect.toBeError('yikes'), + expect.toBeError('[dataloader-codegen :: foo] Expected response to be an array'), + expect.toBeError('yikes'), + expect.toBeError('[dataloader-codegen :: foo] Expected response to be an array'), + expect.toBeError('[dataloader-codegen :: foo] Expected response to be an array'), + ]); + }); +}); + +test('batch endpoint (multiple requests, error handling, with reordering)', async () => { + const config = { + resources: { + foo: { + isBatchResource: true, + docsLink: 'example.com/docs/bar', + batchKey: 'foo_ids', + newKey: 'foo_id', + reorderResultsByKey: 'foo_id', + }, + }, + }; + + const resources = { + foo: ({ foo_ids, include_extra_info }) => { + if (_.isEqual(foo_ids, [1, 3])) { + expect(include_extra_info).toBe(false); + return new Error('yikes'); + } + + if (_.isEqual(foo_ids, [2, 4, 5])) { + expect(include_extra_info).toBe(true); + // return items deliberately out of order + return Promise.resolve([ + { + foo_id: 4, + foo_value: 'greetings', + extra_stuff: 'lorem ipsum', + }, + { + foo_id: 5, + foo_value: 'greetings', + extra_stuff: 'lorem ipsum', + }, + { + foo_id: 2, + foo_value: 'greetings', + extra_stuff: 'lorem ipsum', + }, + ]); + } + }, + }; + + await createDataLoaders(config, async (getLoaders) => { + const loaders = getLoaders(resources); + + const results = await loaders.foo.loadMany([ + { foo_id: 1, include_extra_info: false }, + { foo_id: 2, include_extra_info: true }, + { foo_id: 3, include_extra_info: false }, + { foo_id: 4, include_extra_info: true }, + { foo_id: 5, include_extra_info: true }, + ]); + + expect(results).toMatchObject([ + expect.toBeError(/yikes/), + { foo_id: 2, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, + expect.toBeError(/yikes/), + { foo_id: 4, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, + { foo_id: 5, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, + ]); + }); +}); + +/** + * Without reorderResultsByKey: + * If we requested 3 items, but the resource only returns 2, we don't know which + * response is missing. It's unsafe to return any results, so we must throw an + * error for the whole set of requests. + */ +test('batch endpoint without reorderResultsByKey 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', + }, + }, + }; + + const resources = { + foo: ({ foo_ids, bar }) => { + if (_.isEqual(foo_ids, [1, 2, 3])) { + return Promise.resolve([ + { + foo_id: 1, + foo_value: 'greetings', + extra_stuff: 'lorem ipsum', + }, + // deliberately omit 2 + { + foo_id: 3, + foo_value: 'greetings', + extra_stuff: 'lorem ipsum', + }, + ]); + } else if (_.isEqual(foo_ids, [4])) { + return Promise.resolve([ + { + foo_id: 4, + foo_value: 'greetings', + }, + ]); + } + }, + }; + + await createDataLoaders(config, async (getLoaders) => { + const loaders = getLoaders(resources); + + const results = await loaders.foo.loadMany([ + { foo_id: 1, include_extra_info: true }, + { foo_id: 2, include_extra_info: true }, + { foo_id: 3, include_extra_info: true }, + { foo_id: 4, include_extra_info: false }, + ]); + + expect(results).toMatchObject([ + expect.toBeError( + '[dataloader-codegen :: foo] Resource returned 2 items, but we requested 3 items', + 'BatchItemNotFoundError', + ), + expect.toBeError( + '[dataloader-codegen :: foo] Resource returned 2 items, but we requested 3 items', + 'BatchItemNotFoundError', + ), + expect.toBeError( + '[dataloader-codegen :: foo] Resource returned 2 items, but we requested 3 items', + 'BatchItemNotFoundError', + ), + { foo_id: 4, foo_value: 'greetings' }, + ]); + }); +}); + +test('batch endpoint 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', + }, + }, + }; + + const resources = { + foo: ({ foo_ids, bar }) => { + if (_.isEqual(foo_ids, [1, 2, 3])) { + return Promise.resolve([ + { + foo_id: 1, + foo_value: 'greetings', + extra_stuff: 'lorem ipsum', + }, + // deliberately omit 2 + { + foo_id: 3, + foo_value: 'greetings', + extra_stuff: 'lorem ipsum', + }, + ]); + } else if (_.isEqual(foo_ids, [4])) { + return Promise.resolve([ + { + foo_id: 4, + foo_value: 'greetings', + extra_stuff: 'lorem ipsum', + }, + ]); + } + }, + }; + + await createDataLoaders(config, async (getLoaders) => { + const loaders = getLoaders(resources); + + const results = await loaders.foo.loadMany([ + { foo_id: 1, bar: true }, + { foo_id: 2, bar: true }, + { foo_id: 3, bar: true }, + { foo_id: 4, bar: false }, + ]); + + expect(results).toMatchObject([ + { foo_id: 1, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, + expect.toBeError( + '[dataloader-codegen :: foo] Response did not contain item with foo_id = 2', + 'BatchItemNotFoundError', + ), + { foo_id: 3, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, + { foo_id: 4, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, + ]); + }); +}); + +test('batch endpoint with isResponseDictionary handles a response that returns a dictionary', async () => { + const config = { + resources: { + foo: { + isBatchResource: true, + docsLink: 'example.com/docs/foos', + batchKey: 'foo_ids', + newKey: 'foo_id', + isResponseDictionary: true, + }, + }, + }; + + const resources = { + foo: ({ foo_ids }) => { + expect(foo_ids).toEqual([1, 2, 3]); + return Promise.resolve({ + 2: { foo_id: 2, foo_value: 'world' }, + 1: { foo_id: 1, foo_value: 'hello' }, + 3: { foo_id: 3, foo_value: '!' }, + }); + }, + }; + + await createDataLoaders(config, async (getLoaders) => { + const loaders = getLoaders(resources); + + const results = await loaders.foo.loadMany([{ foo_id: 1 }, { foo_id: 2 }, { foo_id: 3 }]); + expect(results).toEqual([ + { foo_id: 1, foo_value: 'hello' }, + { foo_id: 2, foo_value: 'world' }, + { foo_id: 3, foo_value: '!' }, + ]); + }); +}); + +test('batch endpoint with isResponseDictionary handles a response that returns a dictionary, with a missing item', async () => { + const config = { + resources: { + foo: { + isBatchResource: true, + docsLink: 'example.com/docs/foos', + batchKey: 'foo_ids', + newKey: 'foo_id', + isResponseDictionary: true, + }, + }, + }; + + const resources = { + foo: ({ foo_ids }) => { + expect(foo_ids).toEqual([1, 2, 3]); + return Promise.resolve({ + 1: { foo_id: 1, foo_value: 'hello' }, + 3: { foo_id: 3, foo_value: '!' }, + }); + }, + }; + + await createDataLoaders(config, async (getLoaders) => { + const loaders = getLoaders(resources); + + const results = await loaders.foo.loadMany([{ foo_id: 1 }, { foo_id: 2 }, { foo_id: 3 }]); + expect(results).toEqual([ + { foo_id: 1, foo_value: 'hello' }, + expect.toBeError( + '[dataloader-codegen :: foo] Could not find key = "2" in the response dict', + 'BatchItemNotFoundError', + ), + { foo_id: 3, foo_value: '!' }, + ]); + }); +}); + +test('middleware can transform the request args and the resource response', async () => { + function before(resourcePath, resourceArgs) { + expect(resourcePath).toEqual(['foo']); + expect(resourceArgs).toEqual([{ foo_ids: [100, 200, 300] }]); + + // modify the arguments to the resource + return [{ foo_ids: [1, 2, 3] }]; + } + + function after(resourcePath, response) { + expect(resourcePath).toEqual(['foo']); + expect(response).toEqual([ + { foo_id: 1, foo_value: 'hello' }, + { foo_id: 2, foo_value: 'world' }, + { foo_id: 3, foo_value: '!' }, + ]); + + return [ + { foo_id: 1, foo_value: 'goodbye' }, + { foo_id: 2, foo_value: 'world' }, + { foo_id: 3, foo_value: '?' }, + ]; + } + + const config = { + resources: { + foo: { + isBatchResource: true, + docsLink: 'example.com/docs/bar', + batchKey: 'foo_ids', + newKey: 'foo_id', + }, + }, + }; + + const resources = { + foo: ({ foo_ids }) => { + expect(foo_ids).toEqual([1, 2, 3]); + return Promise.resolve([ + { foo_id: 1, foo_value: 'hello' }, + { foo_id: 2, foo_value: 'world' }, + { foo_id: 3, foo_value: '!' }, + ]); + }, + }; + + await createDataLoaders(config, async (getLoaders) => { + const loaders = getLoaders(resources, { resourceMiddleware: { before, after } }); + + const results = await loaders.foo.loadMany([{ foo_id: 100 }, { foo_id: 200 }, { foo_id: 300 }]); + expect(results).toEqual([ + { foo_id: 1, foo_value: 'goodbye' }, + { foo_id: 2, foo_value: 'world' }, + { foo_id: 3, foo_value: '?' }, + ]); + }); +}); + +test('[isBatchResource: true] returning custom errors from error handler is supported', async () => { + class MyCustomError extends Error { + constructor(...args) { + super(...args); + this.name = this.constructor.name; + this.foo = 'bar'; + Error.captureStackTrace(this, MyCustomError); + } + } + + function errorHandler(resourcePath, error) { + expect(resourcePath).toEqual(['foo']); + expect(error.message).toBe('yikes'); + return new MyCustomError('hello from custom error object'); + } + + const config = { + resources: { + foo: { + isBatchResource: true, + docsLink: 'example.com/docs/bar', + batchKey: 'foo_ids', + newKey: 'foo_id', + }, + }, + }; + + const resources = { + foo: ({ foo_ids, include_extra_info }) => { + if (_.isEqual(foo_ids, [1, 3])) { + expect(include_extra_info).toBe(false); + throw new Error('yikes'); + } + + if (_.isEqual(foo_ids, [2, 4, 5])) { + expect(include_extra_info).toBe(true); + return Promise.resolve([ + { + foo_id: 2, + foo_value: 'greetings', + extra_stuff: 'lorem ipsum', + }, + { + foo_id: 4, + foo_value: 'greetings', + extra_stuff: 'lorem ipsum', + }, + { + foo_id: 5, + foo_value: 'greetings', + extra_stuff: 'lorem ipsum', + }, + ]); + } + }, + }; + + await createDataLoaders(config, async (getLoaders) => { + const loaders = getLoaders(resources, { errorHandler }); + + const results = await loaders.foo.loadMany([ + { foo_id: 1, include_extra_info: false }, + { foo_id: 2, include_extra_info: true }, + { foo_id: 3, include_extra_info: false }, + { foo_id: 4, include_extra_info: true }, + { foo_id: 5, include_extra_info: true }, + ]); + + expect(results).toMatchObject([ + expect.toBeError(/hello from custom error object/, 'MyCustomError'), + { foo_id: 2, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, + expect.toBeError(/hello from custom error object/, 'MyCustomError'), + { foo_id: 4, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, + { foo_id: 5, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, + ]); + + expect(results[0]).toHaveProperty('foo', 'bar'); + expect(results[2]).toHaveProperty('foo', 'bar'); + }); +}); + +test('[isBatchResource: false] returning custom errors from error handler is supported', async () => { + class MyCustomError extends Error { + constructor(...args) { + super(...args); + this.name = this.constructor.name; + this.foo = 'bar'; + Error.captureStackTrace(this, MyCustomError); + } + } + + function errorHandler(resourcePath, error) { + expect(resourcePath).toEqual(['foo']); + expect(error.message).toBe('yikes'); + return new MyCustomError('hello from custom error object'); + } + + const config = { + resources: { + foo: { + isBatchResource: false, + docsLink: 'example.com/docs/bar', + }, + }, + }; + + const resources = { + foo: ({ foo_id, include_extra_info }) => { + if ([1, 3].includes(foo_id)) { + expect(include_extra_info).toBe(false); + throw new Error('yikes'); + } + + if ([2, 4, 5].includes(foo_id)) { + expect(include_extra_info).toBe(true); + return Promise.resolve({ + foo_id, + foo_value: 'greetings', + extra_stuff: 'lorem ipsum', + }); + } + }, + }; + + await createDataLoaders(config, async (getLoaders) => { + const loaders = getLoaders(resources, { errorHandler }); + + const results = await loaders.foo.loadMany([ + { foo_id: 1, include_extra_info: false }, + { foo_id: 2, include_extra_info: true }, + { foo_id: 3, include_extra_info: false }, + { foo_id: 4, include_extra_info: true }, + { foo_id: 5, include_extra_info: true }, + ]); + + expect(results).toMatchObject([ + expect.toBeError(/hello from custom error object/, 'MyCustomError'), + { foo_id: 2, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, + expect.toBeError(/hello from custom error object/, 'MyCustomError'), + { foo_id: 4, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, + { foo_id: 5, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, + ]); + + expect(results[0]).toHaveProperty('foo', 'bar'); + expect(results[2]).toHaveProperty('foo', 'bar'); + }); +}); + +test('bail if errorHandler does not return an error', async () => { + class MyCustomError extends Error { + constructor(...args) { + super(...args); + this.name = this.constructor.name; + this.foo = 'bar'; + Error.captureStackTrace(this, MyCustomError); + } + } + + function errorHandler(resourcePath, error) { + expect(resourcePath).toEqual(['foo']); + expect(error.message).toBe('yikes'); + return 'not an Error object'; + } + + const config = { + resources: { + foo: { + isBatchResource: true, + docsLink: 'example.com/docs/bar', + batchKey: 'foo_ids', + newKey: 'foo_id', + }, + }, + }; + + const resources = { + foo: ({ foo_ids, include_extra_info }) => { + if (_.isEqual(foo_ids, [1, 3])) { + expect(include_extra_info).toBe(false); + throw new Error('yikes'); + } + + if (_.isEqual(foo_ids, [2, 4, 5])) { + expect(include_extra_info).toBe(true); + return Promise.resolve([ + { + foo_id: 2, + foo_value: 'greetings', + extra_stuff: 'lorem ipsum', + }, + { + foo_id: 4, + foo_value: 'greetings', + extra_stuff: 'lorem ipsum', + }, + { + foo_id: 5, + foo_value: 'greetings', + extra_stuff: 'lorem ipsum', + }, + ]); + } + }, + }; + + await createDataLoaders(config, async (getLoaders) => { + const loaders = getLoaders(resources, { errorHandler }); + + const results = await loaders.foo.loadMany([ + { foo_id: 1, include_extra_info: false }, + { foo_id: 2, include_extra_info: true }, + { foo_id: 3, include_extra_info: false }, + { foo_id: 4, include_extra_info: true }, + { foo_id: 5, include_extra_info: true }, + ]); + + expect(results).toMatchObject([ + expect.toBeError(/errorHandler did not return an Error object. Instead, got string: 'not an Error object'/), + { foo_id: 2, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, + expect.toBeError(/errorHandler did not return an Error object. Instead, got string: 'not an Error object'/), + { foo_id: 4, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, + { foo_id: 5, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, + ]); + }); +}); diff --git a/src/implementation.ts b/src/implementation.ts index 89d618f..df164f6 100644 --- a/src/implementation.ts +++ b/src/implementation.ts @@ -166,7 +166,9 @@ function getBatchLoader(resourceConfig: BatchResourceConfig, resourcePath: Reado * We'll refer to each element in the group as a "request ID". */ let requestGroups; - if ('${resourceConfig.secondaryNewKey}' && '${resourceConfig.secondaryBatchKey}') { + if ('${resourceConfig.secondaryNewKey}' !== 'undefined' && '${ + resourceConfig.secondaryBatchKey + }' !== 'undefined') { requestGroups = partitionItemsWithMoreKeys(['${resourceConfig.newKey}', '${ resourceConfig.secondaryNewKey }'], keys); @@ -199,7 +201,7 @@ function getBatchLoader(resourceConfig: BatchResourceConfig, resourcePath: Reado batchKeyParam = `${batchKeyParam}.join(',')`; } - if (secondaryNewKey && secondaryBatchKey) { + if (secondaryNewKey !== undefined && secondaryBatchKey !== undefined) { let secondaryBatchKeyParam = `['${secondaryBatchKey}']: requests.map(k => k['${secondaryNewKey}'])`; if (commaSeparatedBatchKey === true) { secondaryBatchKeyParam = `${secondaryBatchKeyParam}.join(',')`; @@ -225,8 +227,7 @@ function getBatchLoader(resourceConfig: BatchResourceConfig, resourcePath: Reado } })()} let response = await ${callResource(resourceConfig, resourcePath)}(resourceArgs); - console.log('rrrrrr'); - console.log(response instanceof Error); + if (!(response instanceof Error)) { ${(() => { if (typeof resourceConfig.nestedPath === 'string') { @@ -311,7 +312,7 @@ function getBatchLoader(resourceConfig: BatchResourceConfig, resourcePath: Reado if ( !isResponseDictionary && reorderResultsByKey == null && - !(secondaryBatchKey && secondaryNewKey) + !(secondaryBatchKey !== undefined && secondaryNewKey !== undefined) ) { return ` if (!(response instanceof Error)) { @@ -423,11 +424,10 @@ function getBatchLoader(resourceConfig: BatchResourceConfig, resourcePath: Reado return response; })) - console.log('requestGroups'); - console.log(requestGroups); - console.log('groupedResults'); - console.log(groupedResults); - if ('${resourceConfig.secondaryNewKey}' && '${resourceConfig.secondaryBatchKey}') { + + if ('${resourceConfig.secondaryNewKey}' !== 'undefined' && '${ + resourceConfig.secondaryBatchKey + }' !== 'undefined') { const groupByBatchKey = partitionItemsByBatchKey('${resourceConfig.newKey}', ['${ resourceConfig.newKey }', '${resourceConfig.secondaryNewKey}'], keys); diff --git a/src/runtimeHelpers.ts b/src/runtimeHelpers.ts index 88b979b..a02ef15 100644 --- a/src/runtimeHelpers.ts +++ b/src/runtimeHelpers.ts @@ -260,8 +260,6 @@ export function unPartitionResults( * ``` */ const zippedGroups = requestGroups.map((ids, i) => ids.map((id, j) => ({ order: id, result: resultGroups[i][j] }))); - console.log('zippedGroups'); - console.log(zippedGroups); /** * Flatten and sort the groups - e.g.: @@ -276,20 +274,14 @@ export function unPartitionResults( const sortedResults: ReadonlyArray<{ order: number; result: T | Error }> = _.sortBy(_.flatten(zippedGroups), [ 'order', ]); - console.log('sortedResults'); - console.log(sortedResults); // Now that we have a sorted array, return the actual results! return sortedResults .map((r) => r.result) .map((result) => { if (result instanceof CaughtResourceError) { - console.log('nnnnn'); - console.log(result.cause); return result.cause; } else { - console.log('mmmmm'); - console.log(result); return result; } }); From 23a427a77044848aaf1a814081825af2df95c3ef Mon Sep 17 00:00:00 2001 From: Yue Guo Date: Mon, 10 May 2021 19:44:49 -0700 Subject: [PATCH 06/24] rename some functions and add comments for new functions --- src/codegen.ts | 5 +-- src/implementation.ts | 8 ++-- src/runtimeHelpers.ts | 92 ++++++++++++++++++++++++++++--------------- 3 files changed, 66 insertions(+), 39 deletions(-) diff --git a/src/codegen.ts b/src/codegen.ts index 60236fc..fcaa9a5 100644 --- a/src/codegen.ts +++ b/src/codegen.ts @@ -61,13 +61,12 @@ export default function codegen( cacheKeyOptions, CaughtResourceError, defaultErrorHandler, + getBatchKeyForPartitionItems, partitionItems, - partitionItemsByBatchKey, - partitionItemsWithMoreKeys, resultsDictToList, sortByKeys, unPartitionResults, - unPartitionResultsByBatchKeyList, + unPartitionResultsByBatchKeyPartition, } from '${runtimeHelpers}'; diff --git a/src/implementation.ts b/src/implementation.ts index df164f6..9c6dc85 100644 --- a/src/implementation.ts +++ b/src/implementation.ts @@ -169,9 +169,7 @@ function getBatchLoader(resourceConfig: BatchResourceConfig, resourcePath: Reado if ('${resourceConfig.secondaryNewKey}' !== 'undefined' && '${ resourceConfig.secondaryBatchKey }' !== 'undefined') { - requestGroups = partitionItemsWithMoreKeys(['${resourceConfig.newKey}', '${ - resourceConfig.secondaryNewKey - }'], keys); + requestGroups = partitionItems(['${resourceConfig.newKey}', '${resourceConfig.secondaryNewKey}'], keys); } else { requestGroups = partitionItems('${resourceConfig.newKey}', keys); } @@ -428,12 +426,12 @@ function getBatchLoader(resourceConfig: BatchResourceConfig, resourcePath: Reado if ('${resourceConfig.secondaryNewKey}' !== 'undefined' && '${ resourceConfig.secondaryBatchKey }' !== 'undefined') { - const groupByBatchKey = partitionItemsByBatchKey('${resourceConfig.newKey}', ['${ + const batchKeyPartition = getBatchKeyForPartitionItems('${resourceConfig.newKey}', ['${ resourceConfig.newKey }', '${resourceConfig.secondaryNewKey}'], keys); // Split the results back up into the order that they were requested - return unPartitionResultsByBatchKeyList(groupByBatchKey, requestGroups, groupedResults); + return unPartitionResultsByBatchKeyPartition(batchKeyPartition, requestGroups, groupedResults); }else { return unPartitionResults(requestGroups, groupedResults); } diff --git a/src/runtimeHelpers.ts b/src/runtimeHelpers.ts index a02ef15..17bd7ee 100644 --- a/src/runtimeHelpers.ts +++ b/src/runtimeHelpers.ts @@ -68,11 +68,11 @@ export const cacheKeyOptions = { * * Example: * ```js - * partitionItems([ + * partitionItems('bar_id', [ * { bar_id: 2, include_extra_info: true }, * { bar_id: 3, include_extra_info: false }, * { bar_id: 4, include_extra_info: true }, - * ], 'bar_id') + * ]) * ``` * * Returns: @@ -80,29 +80,15 @@ export const cacheKeyOptions = { * * TODO: add generic instead of 'object' for the items array argument */ -export function partitionItems(ignoreKey: string, items: ReadonlyArray): ReadonlyArray> { - const groups: { - [key: string]: Array; - } = {}; - - items.forEach((item, i) => { - const hash = objectHash(_.omit(item, ignoreKey), { algorithm: 'passthrough' }); - groups[hash] = groups[hash] || []; - groups[hash].push(i); - }); - - return Object.values(groups); -} - -export function partitionItemsWithMoreKeys( - ignoreKey: Array, +export function partitionItems( + ignoreKeys: Array | string, items: ReadonlyArray, ): ReadonlyArray> { const groups: { [key: string]: Array; } = {}; items.forEach((item, i) => { - const hash = objectHash(_.omit(item, ignoreKey), { algorithm: 'passthrough' }); + const hash = objectHash(_.omit(item, ignoreKeys), { algorithm: 'passthrough' }); groups[hash] = groups[hash] || []; groups[hash].push(i); }); @@ -110,19 +96,38 @@ export function partitionItemsWithMoreKeys( return Object.values(groups); } -export function partitionItemsByBatchKey( +/** + * This function is only called when we have secondaryBatchKey, and it's + * used to map result to the order of requests. + * + * Example: + * ```js + * partitionItems( + * 'bar_id', + * ['bar_id', 'property'], + * [ + * { bar_id: 2, property: 'name', include_extra_info: true }, + * { bar_id: 3, property: 'rating', include_extra_info: false }, + * { bar_id: 2, property: 'rating', include_extra_info: true }, + * ]) + * ``` + * + * Returns: + * `[ [ 2, 2 ], [ 3 ] ]` + * + * TODO: add generic instead of 'object' for the items array argument + */ +export function getBatchKeyForPartitionItems( batchKey: string, - ignoreKey: Array, + ignoreKeys: Array, items: ReadonlyArray, ): ReadonlyArray> { const groups: { [key: string]: Array; } = {}; items.forEach((item, i) => { - const hash = objectHash(_.omit(item, ignoreKey), { algorithm: 'passthrough' }); + const hash = objectHash(_.omit(item, ignoreKeys), { algorithm: 'passthrough' }); groups[hash] = groups[hash] || []; - // let map = {}; - // map[i] = items[i][batchKey]; groups[hash].push(items[i][batchKey]); }); @@ -287,7 +292,32 @@ export function unPartitionResults( }); } -export function unPartitionResultsByBatchKeyList( +/** + * Perform the inverse mapping from partitionItems on the nested results we get + * back from the service. This function is only called when we have secondaryBatchKey. + * We assume the batchKey is inside the response here. + * + * Example + * ```js + * unPartitionResultsByBatchKeyList( + * [ [2, 2], [1] ] + * [ [0, 2], [1] ], + * [ + * [ { bar_id: 2, name: 'Burger King', rating: 3 } ], + * [ { bar_id: 1, name: 'In N Out', rating: 4 } ] + * ], + * ) + * ``` + * + * Returns: + * ``` + * [ + * { bar_id: 2, name: 'Burger King', rating: 3 }, + * { bar_id: 1, name: 'In N Out', rating: 4 }, + * { bar_id: 2, name: 'Burger King', rating: 3 }, + * ] + */ +export function unPartitionResultsByBatchKeyPartition( groupByBatchKey, /** Should be a nested array of IDs, as generated by partitionItems */ requestGroups: ReadonlyArray>, @@ -299,11 +329,11 @@ export function unPartitionResultsByBatchKeyList( * ```js * [ * [ - * { order: 0, result: { foo: 'foo' } }, - * { order: 2, result: { bar: 'bar' } }, + * { order: 0, result: { bar_id: 2, name: 'Burger King', rating: 3 }, + * { order: 2, result: { bar_id: 2, name: 'Burger King', rating: 3 } }, * ], * [ - * { order: 1, result: { baz: 'baz' } }, + * { order: 1, result: { bar_id: 1, name: 'In N Out', rating: 4 } }, * ] * ] * ``` @@ -321,9 +351,9 @@ export function unPartitionResultsByBatchKeyList( * Flatten and sort the groups - e.g.: * ```js * [ - * { order: 0, result: { foo: 'foo' } }, - * { order: 1, result: { baz: 'baz' } }, - * { order: 2, result: { bar: 'bar' } } + * { order: 0, result: { bar_id: 2, name: 'Burger King', rating: 3 } }, + * { order: 1, result: { bar_id: 1, name: 'In N Out', rating: 4 } }, + * { order: 2, result: { bar_id: 2, name: 'Burger King', rating: 3 } } * ] * ``` */ From b286a3928dab4166a7fff9e83d0a4dc718fe719b Mon Sep 17 00:00:00 2001 From: Yue Guo Date: Tue, 11 May 2021 10:02:24 -0700 Subject: [PATCH 07/24] edit documentation for new functions --- __tests__/implementation.test.js | 75 +++++++++++++++++++++++++++----- src/implementation.ts | 2 +- src/runtimeHelpers.ts | 15 ++++--- 3 files changed, 76 insertions(+), 16 deletions(-) diff --git a/__tests__/implementation.test.js b/__tests__/implementation.test.js index 977cbc6..2a735e4 100644 --- a/__tests__/implementation.test.js +++ b/__tests__/implementation.test.js @@ -311,8 +311,8 @@ test('batch endpoint (multiple requests) with secondaryBatchKey', async () => { if (_.isEqual(foo_ids, [2, 1])) { expect(include_extra_info).toBe(false); return Promise.resolve([ - { foo_id: 1, photo: 'photo1', id: 'id1', name: 'name1' }, - { foo_id: 2, photo: 'photo2', id: 'id2', name: 'name2' }, + { foo_id: 1, rating: 3, name: 'Burger King' }, + { foo_id: 2, rating: 4, name: 'In N Out' }, ]); } @@ -321,9 +321,8 @@ test('batch endpoint (multiple requests) with secondaryBatchKey', async () => { return Promise.resolve([ { foo_id: 3, - photo: 'photo3', - id: 'id3', - name: 'name3', + rating: 5, + name: 'Shake Shack', extra_stuff: 'lorem ipsum', }, ]); @@ -336,14 +335,70 @@ test('batch endpoint (multiple requests) with secondaryBatchKey', async () => { const results = await loaders.foo.loadMany([ { foo_id: 2, property: 'name', include_extra_info: false }, - { foo_id: 1, property: 'photo', include_extra_info: false }, - { foo_id: 3, property: 'photo', include_extra_info: true }, + { foo_id: 1, property: 'rating', include_extra_info: false }, + { foo_id: 3, property: 'rating', include_extra_info: true }, ]); expect(results).toEqual([ - { foo_id: 2, photo: 'photo2', id: 'id2', name: 'name2' }, - { foo_id: 1, photo: 'photo1', id: 'id1', name: 'name1' }, - { foo_id: 3, photo: 'photo3', id: 'id3', name: 'name3', extra_stuff: 'lorem ipsum' }, + { foo_id: 2, rating: 4, name: 'In N Out' }, + { foo_id: 1, rating: 3, name: 'Burger King' }, + { foo_id: 3, rating: 5, name: 'Shake Shack', extra_stuff: 'lorem ipsum' }, + ]); + }); +}); + +test('batch endpoint (multiple requests) with secondaryBatchKey different response structure', async () => { + const config = { + resources: { + foo: { + isBatchResource: true, + docsLink: 'example.com/docs/bar', + batchKey: 'foo_ids', + newKey: 'foo_id', + secondaryBatchKey: 'properties', + secondaryNewKey: 'property', + }, + }, + }; + + const resources = { + foo: ({ foo_ids, properties, include_extra_info }) => { + if (_.isEqual(foo_ids, [2, 1])) { + expect(include_extra_info).toBe(false); + return Promise.resolve([ + { 1: { rating: 3, name: 'Burger King' } }, + { 2: { rating: 4, name: 'In N Out' } }, + ]); + } + + if (_.isEqual(foo_ids, [3])) { + expect(include_extra_info).toBe(true); + return Promise.resolve([ + { + 3: { + rating: 5, + name: 'Shake Shack', + extra_stuff: 'lorem ipsum', + }, + }, + ]); + } + }, + }; + + await createDataLoaders(config, async (getLoaders) => { + const loaders = getLoaders(resources); + + const results = await loaders.foo.loadMany([ + { foo_id: 2, property: 'name', include_extra_info: false }, + { foo_id: 1, property: 'rating', include_extra_info: false }, + { foo_id: 3, property: 'rating', include_extra_info: true }, + ]); + + expect(results).toEqual([ + { 2: { rating: 4, name: 'In N Out' } }, + { 1: { rating: 3, name: 'Burger King' } }, + { 3: { rating: 5, name: 'Shake Shack', extra_stuff: 'lorem ipsum' } }, ]); }); }); diff --git a/src/implementation.ts b/src/implementation.ts index 9c6dc85..140697c 100644 --- a/src/implementation.ts +++ b/src/implementation.ts @@ -429,10 +429,10 @@ function getBatchLoader(resourceConfig: BatchResourceConfig, resourcePath: Reado const batchKeyPartition = getBatchKeyForPartitionItems('${resourceConfig.newKey}', ['${ resourceConfig.newKey }', '${resourceConfig.secondaryNewKey}'], keys); - // Split the results back up into the order that they were requested return unPartitionResultsByBatchKeyPartition(batchKeyPartition, requestGroups, groupedResults); }else { + // Split the results back up into the order that they were requested return unPartitionResults(requestGroups, groupedResults); } }, diff --git a/src/runtimeHelpers.ts b/src/runtimeHelpers.ts index 17bd7ee..936536c 100644 --- a/src/runtimeHelpers.ts +++ b/src/runtimeHelpers.ts @@ -62,7 +62,7 @@ export const cacheKeyOptions = { /** * Take in all objects passed to .load(), and bucket them by the non - * batchKey attributes. + * batchKeys(including batchKey and secondaryBatchKey) attributes. * * We use this to chunk up the requests to the resource. * @@ -97,12 +97,16 @@ export function partitionItems( } /** + * Take in all objects passed to .load(), and bucket them by the non + * batchKeys(including batchKey and secondaryBatchKey) attributes. + * Return batchKey value for each partition items. + * * This function is only called when we have secondaryBatchKey, and it's * used to map result to the order of requests. * * Example: * ```js - * partitionItems( + * getBatchKeyForPartitionItems( * 'bar_id', * ['bar_id', 'property'], * [ @@ -299,7 +303,7 @@ export function unPartitionResults( * * Example * ```js - * unPartitionResultsByBatchKeyList( + * unPartitionResultsByBatchKeyPartition( * [ [2, 2], [1] ] * [ [0, 2], [1] ], * [ @@ -318,7 +322,7 @@ export function unPartitionResults( * ] */ export function unPartitionResultsByBatchKeyPartition( - groupByBatchKey, + batchKeyPartition, /** Should be a nested array of IDs, as generated by partitionItems */ requestGroups: ReadonlyArray>, /** The results back from the service, in the same shape as groups */ @@ -341,7 +345,8 @@ export function unPartitionResultsByBatchKeyPartition( const zippedGroups = requestGroups.map(function (ids, i) { return ids.map(function (id, j) { for (const element of Object.values(resultGroups)[i]) { - if (groupByBatchKey[i][j] in element || Object.values(element).includes(groupByBatchKey[i][j])) { + // We assume the batchKey is inside the response, either as a key or a value + if (batchKeyPartition[i][j] in element || Object.values(element).includes(batchKeyPartition[i][j])) { return { order: id, result: element }; } } From 2d4a6bd69b14537aa8981b5406350de372db6cfe Mon Sep 17 00:00:00 2001 From: Yue Guo Date: Wed, 12 May 2021 14:14:12 -0700 Subject: [PATCH 08/24] data masking --- __tests__/implementation.test.js | 17 ++++----- src/codegen.ts | 2 +- src/implementation.ts | 27 ++++++++------ src/runtimeHelpers.ts | 60 ++++++++++++++++++++++---------- 4 files changed, 64 insertions(+), 42 deletions(-) diff --git a/__tests__/implementation.test.js b/__tests__/implementation.test.js index 2a735e4..e5fe14d 100644 --- a/__tests__/implementation.test.js +++ b/__tests__/implementation.test.js @@ -340,9 +340,9 @@ test('batch endpoint (multiple requests) with secondaryBatchKey', async () => { ]); expect(results).toEqual([ - { foo_id: 2, rating: 4, name: 'In N Out' }, - { foo_id: 1, rating: 3, name: 'Burger King' }, - { foo_id: 3, rating: 5, name: 'Shake Shack', extra_stuff: 'lorem ipsum' }, + { foo_id: 2, name: 'In N Out' }, + { foo_id: 1, rating: 3 }, + { foo_id: 3, rating: 5 }, ]); }); }); @@ -363,7 +363,7 @@ test('batch endpoint (multiple requests) with secondaryBatchKey different respon const resources = { foo: ({ foo_ids, properties, include_extra_info }) => { - if (_.isEqual(foo_ids, [2, 1])) { + if (_.isEqual(foo_ids, [2, 1]) && _.isEqual(properties, ['name', 'rating'])) { expect(include_extra_info).toBe(false); return Promise.resolve([ { 1: { rating: 3, name: 'Burger King' } }, @@ -371,13 +371,12 @@ test('batch endpoint (multiple requests) with secondaryBatchKey different respon ]); } - if (_.isEqual(foo_ids, [3])) { + if (_.isEqual(foo_ids, [3]) && _.isEqual(properties, ['rating'])) { expect(include_extra_info).toBe(true); return Promise.resolve([ { 3: { rating: 5, - name: 'Shake Shack', extra_stuff: 'lorem ipsum', }, }, @@ -395,11 +394,7 @@ test('batch endpoint (multiple requests) with secondaryBatchKey different respon { foo_id: 3, property: 'rating', include_extra_info: true }, ]); - expect(results).toEqual([ - { 2: { rating: 4, name: 'In N Out' } }, - { 1: { rating: 3, name: 'Burger King' } }, - { 3: { rating: 5, name: 'Shake Shack', extra_stuff: 'lorem ipsum' } }, - ]); + expect(results).toEqual([{ 2: { name: 'In N Out' } }, { 1: { rating: 3 } }, { 3: { rating: 5 } }]); }); }); diff --git a/src/codegen.ts b/src/codegen.ts index fcaa9a5..7102225 100644 --- a/src/codegen.ts +++ b/src/codegen.ts @@ -61,7 +61,7 @@ export default function codegen( cacheKeyOptions, CaughtResourceError, defaultErrorHandler, - getBatchKeyForPartitionItems, + getBatchKeysForPartitionItems, partitionItems, resultsDictToList, sortByKeys, diff --git a/src/implementation.ts b/src/implementation.ts index 140697c..0526014 100644 --- a/src/implementation.ts +++ b/src/implementation.ts @@ -166,9 +166,9 @@ function getBatchLoader(resourceConfig: BatchResourceConfig, resourcePath: Reado * We'll refer to each element in the group as a "request ID". */ let requestGroups; - if ('${resourceConfig.secondaryNewKey}' !== 'undefined' && '${ - resourceConfig.secondaryBatchKey - }' !== 'undefined') { + if (${typeof resourceConfig.secondaryNewKey === 'string'} && ${ + typeof resourceConfig.secondaryBatchKey === 'string' + }) { requestGroups = partitionItems(['${resourceConfig.newKey}', '${resourceConfig.secondaryNewKey}'], keys); } else { requestGroups = partitionItems('${resourceConfig.newKey}', keys); @@ -199,7 +199,7 @@ function getBatchLoader(resourceConfig: BatchResourceConfig, resourcePath: Reado batchKeyParam = `${batchKeyParam}.join(',')`; } - if (secondaryNewKey !== undefined && secondaryBatchKey !== undefined) { + if (typeof secondaryNewKey === 'string' && typeof secondaryBatchKey === 'string') { let secondaryBatchKeyParam = `['${secondaryBatchKey}']: requests.map(k => k['${secondaryNewKey}'])`; if (commaSeparatedBatchKey === true) { secondaryBatchKeyParam = `${secondaryBatchKeyParam}.join(',')`; @@ -310,7 +310,7 @@ function getBatchLoader(resourceConfig: BatchResourceConfig, resourcePath: Reado if ( !isResponseDictionary && reorderResultsByKey == null && - !(secondaryBatchKey !== undefined && secondaryNewKey !== undefined) + !(typeof secondaryNewKey === 'string' && typeof secondaryBatchKey === 'string') ) { return ` if (!(response instanceof Error)) { @@ -423,15 +423,20 @@ function getBatchLoader(resourceConfig: BatchResourceConfig, resourcePath: Reado return response; })) - if ('${resourceConfig.secondaryNewKey}' !== 'undefined' && '${ - resourceConfig.secondaryBatchKey - }' !== 'undefined') { - const batchKeyPartition = getBatchKeyForPartitionItems('${resourceConfig.newKey}', ['${ + if (${typeof resourceConfig.secondaryNewKey === 'string'} && ${ + typeof resourceConfig.secondaryBatchKey === 'string' + }) { + const batchKeyPartition = getBatchKeysForPartitionItems('${resourceConfig.newKey}', ['${ resourceConfig.newKey }', '${resourceConfig.secondaryNewKey}'], keys); + const secondaryBatchKeyPartiion = getBatchKeysForPartitionItems('${ + resourceConfig.secondaryNewKey + }', ['${resourceConfig.newKey}', '${resourceConfig.secondaryNewKey}'], keys); // Split the results back up into the order that they were requested - return unPartitionResultsByBatchKeyPartition(batchKeyPartition, requestGroups, groupedResults); - }else { + return unPartitionResultsByBatchKeyPartition('${ + resourceConfig.newKey + }', batchKeyPartition, secondaryBatchKeyPartiion, requestGroups, groupedResults); + } else { // Split the results back up into the order that they were requested return unPartitionResults(requestGroups, groupedResults); } diff --git a/src/runtimeHelpers.ts b/src/runtimeHelpers.ts index 936536c..b3f5088 100644 --- a/src/runtimeHelpers.ts +++ b/src/runtimeHelpers.ts @@ -62,7 +62,7 @@ export const cacheKeyOptions = { /** * Take in all objects passed to .load(), and bucket them by the non - * batchKeys(including batchKey and secondaryBatchKey) attributes. + * batch keys (i.e. `batchKey` and `secondaryBatchKey`) attributes. * * We use this to chunk up the requests to the resource. * @@ -87,6 +87,7 @@ export function partitionItems( const groups: { [key: string]: Array; } = {}; + items.forEach((item, i) => { const hash = objectHash(_.omit(item, ignoreKeys), { algorithm: 'passthrough' }); groups[hash] = groups[hash] || []; @@ -98,8 +99,8 @@ export function partitionItems( /** * Take in all objects passed to .load(), and bucket them by the non - * batchKeys(including batchKey and secondaryBatchKey) attributes. - * Return batchKey value for each partition items. + * batch keys (i.e. `batchKey` and `secondaryBatchKey`) attributes. + * Return batch keys value for each partition items. * * This function is only called when we have secondaryBatchKey, and it's * used to map result to the order of requests. @@ -121,7 +122,7 @@ export function partitionItems( * * TODO: add generic instead of 'object' for the items array argument */ -export function getBatchKeyForPartitionItems( +export function getBatchKeysForPartitionItems( batchKey: string, ignoreKeys: Array, items: ReadonlyArray, @@ -299,12 +300,14 @@ export function unPartitionResults( /** * Perform the inverse mapping from partitionItems on the nested results we get * back from the service. This function is only called when we have secondaryBatchKey. - * We assume the batchKey is inside the response here. + * We assume the newKey is inside the response here. * * Example * ```js * unPartitionResultsByBatchKeyPartition( - * [ [2, 2], [1] ] + * 'bard_id', + * [ ['name', 'rating'], ['rating'] ], + * [ [2, 2], [1] ], * [ [0, 2], [1] ], * [ * [ { bar_id: 2, name: 'Burger King', rating: 3 } ], @@ -316,13 +319,15 @@ export function unPartitionResults( * Returns: * ``` * [ - * { bar_id: 2, name: 'Burger King', rating: 3 }, - * { bar_id: 1, name: 'In N Out', rating: 4 }, - * { bar_id: 2, name: 'Burger King', rating: 3 }, + * { bar_id: 2, name: 'Burger King' }, + * { bar_id: 1, rating: 4 }, + * { bar_id: 2, rating: 3 }, * ] */ export function unPartitionResultsByBatchKeyPartition( - batchKeyPartition, + newKey: string, + batchKeyPartition: ReadonlyArray>, + secondaryBatchKeyPartion: ReadonlyArray>, /** Should be a nested array of IDs, as generated by partitionItems */ requestGroups: ReadonlyArray>, /** The results back from the service, in the same shape as groups */ @@ -333,11 +338,11 @@ export function unPartitionResultsByBatchKeyPartition( * ```js * [ * [ - * { order: 0, result: { bar_id: 2, name: 'Burger King', rating: 3 }, - * { order: 2, result: { bar_id: 2, name: 'Burger King', rating: 3 } }, + * { order: 0, result: { bar_id: 2, name: 'Burger King' }, + * { order: 2, result: { bar_id: 2, rating: 3 } }, * ], * [ - * { order: 1, result: { bar_id: 1, name: 'In N Out', rating: 4 } }, + * { order: 1, result: { bar_id: 1, rating: 4 } }, * ] * ] * ``` @@ -345,9 +350,26 @@ export function unPartitionResultsByBatchKeyPartition( const zippedGroups = requestGroups.map(function (ids, i) { return ids.map(function (id, j) { for (const element of Object.values(resultGroups)[i]) { - // We assume the batchKey is inside the response, either as a key or a value - if (batchKeyPartition[i][j] in element || Object.values(element).includes(batchKeyPartition[i][j])) { - return { order: id, result: element }; + // case 1, newKey has its own key-value pair in the response, + // { bar_id: 2, name: 'Burger King', rating: 3 } + if (Object.values(element).includes(batchKeyPartition[i][j])) { + const updatedElement = Object.assign( + {}, + ...[newKey, secondaryBatchKeyPartion[i][j]].map((key) => ({ [key]: element[key] })), + ); + return { order: id, result: updatedElement }; + } + // case 2, the value of newKey is the key and its properties are its values. + // { 2: { name: 'Burger King', rating: 3 }} + else if (batchKeyPartition[i][j] in element) { + const updatedElement = { + [batchKeyPartition[i][j]]: { + [secondaryBatchKeyPartion[i][j]]: + element[batchKeyPartition[i][j]][secondaryBatchKeyPartion[i][j]], + }, + }; + // const updatedElement = Object.assign({}, ...[newKey, secondaryBatchKeyPartion[i][j]].map(key => ({[key]: element[key]}))) + return { order: id, result: updatedElement }; } } }); @@ -356,9 +378,9 @@ export function unPartitionResultsByBatchKeyPartition( * Flatten and sort the groups - e.g.: * ```js * [ - * { order: 0, result: { bar_id: 2, name: 'Burger King', rating: 3 } }, - * { order: 1, result: { bar_id: 1, name: 'In N Out', rating: 4 } }, - * { order: 2, result: { bar_id: 2, name: 'Burger King', rating: 3 } } + * { order: 0, result: { bar_id: 2, name: 'Burger King' } }, + * { order: 1, result: { bar_id: 1, rating: 4 } }, + * { order: 2, result: { bar_id: 2, rating: 3 } } * ] * ``` */ From 6116e05e24827a37e4e14ac63e4a9c373a3131e3 Mon Sep 17 00:00:00 2001 From: Yue Guo Date: Wed, 12 May 2021 14:21:06 -0700 Subject: [PATCH 09/24] remove console.log in /examples/swapi --- examples/swapi/swapi-loaders.js | 122 ++++++++++++++++++++++++-------- 1 file changed, 92 insertions(+), 30 deletions(-) diff --git a/examples/swapi/swapi-loaders.js b/examples/swapi/swapi-loaders.js index 3764cb0..e6bb10e 100644 --- a/examples/swapi/swapi-loaders.js +++ b/examples/swapi/swapi-loaders.js @@ -13,10 +13,12 @@ import { cacheKeyOptions, CaughtResourceError, defaultErrorHandler, + getBatchKeysForPartitionItems, partitionItems, resultsDictToList, sortByKeys, unPartitionResults, + unPartitionResultsByBatchKeyPartition, } from 'dataloader-codegen/lib/runtimeHelpers'; /** @@ -276,9 +278,12 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade * * We'll refer to each element in the group as a "request ID". */ - const requestGroups = partitionItems('planet_id', keys); - console.log('lllllll'); - console.log(requestGroups); + let requestGroups; + if (false && false) { + requestGroups = partitionItems(['planet_id', 'undefined'], keys); + } else { + requestGroups = partitionItems('planet_id', keys); + } // Map the request groups to a list of Promises - one for each request const groupedResults = await Promise.all( @@ -301,8 +306,6 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade }, ]; - console.log('fffffffff'); - console.log(resourceArgs); let response = await (async (_resourceArgs) => { // Make a re-assignable variable so flow/eslint doesn't complain let __resourceArgs = _resourceArgs; @@ -349,8 +352,7 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade return _response; })(resourceArgs); - console.log('ggggggg'); - console.log(response); + if (!(response instanceof Error)) { } @@ -382,7 +384,6 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade 'Add reorderResultsByKey to the config for this resource to be able to handle a partial response.', ].join(' '), ); - // Tell flow that BatchItemNotFoundError extends Error. // It's an issue with flowgen package, but not an issue with Flow. // @see https://github.com/Yelp/dataloader-codegen/pull/35#discussion_r394777533 @@ -431,8 +432,29 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade }), ); - // Split the results back up into the order that they were requested - return unPartitionResults(requestGroups, groupedResults); + if (false && false) { + const batchKeyPartition = getBatchKeysForPartitionItems( + 'planet_id', + ['planet_id', 'undefined'], + keys, + ); + const secondaryBatchKeyPartiion = getBatchKeysForPartitionItems( + 'undefined', + ['planet_id', 'undefined'], + keys, + ); + // Split the results back up into the order that they were requested + return unPartitionResultsByBatchKeyPartition( + 'planet_id', + batchKeyPartition, + secondaryBatchKeyPartiion, + requestGroups, + groupedResults, + ); + } else { + // Split the results back up into the order that they were requested + return unPartitionResults(requestGroups, groupedResults); + } }, { ...cacheKeyOptions, @@ -555,9 +577,12 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade * * We'll refer to each element in the group as a "request ID". */ - const requestGroups = partitionItems('person_id', keys); - console.log('lllllll'); - console.log(requestGroups); + let requestGroups; + if (false && false) { + requestGroups = partitionItems(['person_id', 'undefined'], keys); + } else { + requestGroups = partitionItems('person_id', keys); + } // Map the request groups to a list of Promises - one for each request const groupedResults = await Promise.all( @@ -580,8 +605,6 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade }, ]; - console.log('fffffffff'); - console.log(resourceArgs); let response = await (async (_resourceArgs) => { // Make a re-assignable variable so flow/eslint doesn't complain let __resourceArgs = _resourceArgs; @@ -625,8 +648,7 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade return _response; })(resourceArgs); - console.log('ggggggg'); - console.log(response); + if (!(response instanceof Error)) { } @@ -658,7 +680,6 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade 'Add reorderResultsByKey to the config for this resource to be able to handle a partial response.', ].join(' '), ); - // Tell flow that BatchItemNotFoundError extends Error. // It's an issue with flowgen package, but not an issue with Flow. // @see https://github.com/Yelp/dataloader-codegen/pull/35#discussion_r394777533 @@ -707,8 +728,29 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade }), ); - // Split the results back up into the order that they were requested - return unPartitionResults(requestGroups, groupedResults); + if (false && false) { + const batchKeyPartition = getBatchKeysForPartitionItems( + 'person_id', + ['person_id', 'undefined'], + keys, + ); + const secondaryBatchKeyPartiion = getBatchKeysForPartitionItems( + 'undefined', + ['person_id', 'undefined'], + keys, + ); + // Split the results back up into the order that they were requested + return unPartitionResultsByBatchKeyPartition( + 'person_id', + batchKeyPartition, + secondaryBatchKeyPartiion, + requestGroups, + groupedResults, + ); + } else { + // Split the results back up into the order that they were requested + return unPartitionResults(requestGroups, groupedResults); + } }, { ...cacheKeyOptions, @@ -831,9 +873,12 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade * * We'll refer to each element in the group as a "request ID". */ - const requestGroups = partitionItems('vehicle_id', keys); - console.log('lllllll'); - console.log(requestGroups); + let requestGroups; + if (false && false) { + requestGroups = partitionItems(['vehicle_id', 'undefined'], keys); + } else { + requestGroups = partitionItems('vehicle_id', keys); + } // Map the request groups to a list of Promises - one for each request const groupedResults = await Promise.all( @@ -856,8 +901,6 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade }, ]; - console.log('fffffffff'); - console.log(resourceArgs); let response = await (async (_resourceArgs) => { // Make a re-assignable variable so flow/eslint doesn't complain let __resourceArgs = _resourceArgs; @@ -904,8 +947,7 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade return _response; })(resourceArgs); - console.log('ggggggg'); - console.log(response); + if (!(response instanceof Error)) { } @@ -937,7 +979,6 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade 'Add reorderResultsByKey to the config for this resource to be able to handle a partial response.', ].join(' '), ); - // Tell flow that BatchItemNotFoundError extends Error. // It's an issue with flowgen package, but not an issue with Flow. // @see https://github.com/Yelp/dataloader-codegen/pull/35#discussion_r394777533 @@ -986,8 +1027,29 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade }), ); - // Split the results back up into the order that they were requested - return unPartitionResults(requestGroups, groupedResults); + if (false && false) { + const batchKeyPartition = getBatchKeysForPartitionItems( + 'vehicle_id', + ['vehicle_id', 'undefined'], + keys, + ); + const secondaryBatchKeyPartiion = getBatchKeysForPartitionItems( + 'undefined', + ['vehicle_id', 'undefined'], + keys, + ); + // Split the results back up into the order that they were requested + return unPartitionResultsByBatchKeyPartition( + 'vehicle_id', + batchKeyPartition, + secondaryBatchKeyPartiion, + requestGroups, + groupedResults, + ); + } else { + // Split the results back up into the order that they were requested + return unPartitionResults(requestGroups, groupedResults); + } }, { ...cacheKeyOptions, From 88f8b6dfa7c52865308d80ee517fd76842110c2a Mon Sep 17 00:00:00 2001 From: Yue Guo Date: Wed, 19 May 2021 11:56:26 -0700 Subject: [PATCH 10/24] add error handling --- __tests__/implementation.test.js | 191 +++++++++++++++++++++++++++++++ src/runtimeHelpers.ts | 76 +++++++----- 2 files changed, 238 insertions(+), 29 deletions(-) diff --git a/__tests__/implementation.test.js b/__tests__/implementation.test.js index e5fe14d..1a057aa 100644 --- a/__tests__/implementation.test.js +++ b/__tests__/implementation.test.js @@ -347,6 +347,197 @@ test('batch endpoint (multiple requests) with secondaryBatchKey', async () => { }); }); +test('batch endpoint (multiple requests) with secondaryBatchKey that rejects', async () => { + const config = { + resources: { + foo: { + isBatchResource: true, + docsLink: 'example.com/docs/bar', + batchKey: 'foo_ids', + newKey: 'foo_id', + secondaryBatchKey: 'properties', + secondaryNewKey: 'property', + }, + }, + }; + + const resources = { + foo: ({ foo_ids, properties, include_extra_info }) => { + if (_.isEqual(foo_ids, [1, 3])) { + expect(include_extra_info).toBe(false); + return Promise.reject('yikes'); + } + if (_.isEqual(foo_ids, [2, 4, 5])) { + expect(include_extra_info).toBe(true); + return Promise.resolve([ + { + foo_id: 2, + name: 'Burger King', + rating: 3, + }, + { + foo_id: 4, + name: 'In N Out', + rating: 3.5, + }, + { + foo_id: 5, + name: 'Shake Shack', + rating: 4, + extra_stuff: 'lorem ipsum', + }, + ]); + } + }, + }; + + await createDataLoaders(config, async (getLoaders) => { + const loaders = getLoaders(resources); + + const results = await loaders.foo.loadMany([ + { foo_id: 1, property: 'name', include_extra_info: false }, + { foo_id: 2, property: 'rating', include_extra_info: true }, + { foo_id: 3, property: 'name', include_extra_info: false }, + { foo_id: 4, property: 'rating', include_extra_info: true }, + { foo_id: 5, property: 'name', include_extra_info: true }, + ]); + + expect(results).toMatchObject([ + expect.toBeError(/yikes/, 'NonError'), + { foo_id: 2, rating: 3 }, + expect.toBeError(/yikes/, 'NonError'), + { foo_id: 4, rating: 3.5 }, + { foo_id: 5, name: 'Shake Shack' }, + ]); + }); +}); + +test('batch endpoint (multiple requests) with secondaryBatchKey error handling', async () => { + const config = { + resources: { + foo: { + isBatchResource: true, + docsLink: 'example.com/docs/bar', + batchKey: 'foo_ids', + newKey: 'foo_id', + secondaryBatchKey: 'properties', + secondaryNewKey: 'property', + }, + }, + }; + + 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([ + { + foo_id: 2, + name: 'Burger King', + rating: 3, + }, + { + foo_id: 4, + name: 'In N Out', + rating: 3.5, + }, + { + foo_id: 5, + name: 'Shake Shack', + rating: 4, + extra_stuff: 'lorem ipsum', + }, + ]); + } + 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, property: 'name', include_extra_info: false }, + { foo_id: 2, property: 'rating', include_extra_info: true }, + { foo_id: 3, property: 'name', include_extra_info: false }, + { foo_id: 4, property: 'rating', include_extra_info: true }, + { foo_id: 5, property: 'name', include_extra_info: true }, + ]); + + expect(results).toMatchObject([ + expect.toBeError(/yikes/), + { foo_id: 2, rating: 3 }, + expect.toBeError(/yikes/), + { foo_id: 4, rating: 3.5 }, + { foo_id: 5, name: 'Shake Shack' }, + ]); + }); +}); + +test('batch endpoint with secondaryBatchKey without reorderResultsByKey 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', + secondaryBatchKey: 'properties', + secondaryNewKey: 'property', + }, + }, + }; + + const resources = { + foo: ({ foo_ids, bar }) => { + if (_.isEqual(foo_ids, [1, 2, 3])) { + 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])) { + 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, property: 'name', include_extra_info: true }, + { foo_id: 2, property: 'rating', include_extra_info: true }, + { foo_id: 3, property: 'rating', include_extra_info: true }, + { foo_id: 4, property: 'rating', include_extra_info: false }, + ]); + + expect(results).toMatchObject([ + { foo_id: 1, name: 'Shake Shack' }, + expect.toBeError('Could not find key = "2" in the response dict.', 'BatchItemNotFoundError'), + { foo_id: 3, rating: 3 }, + { foo_id: 4, rating: 3.5 }, + ]); + }); +}); + test('batch endpoint (multiple requests) with secondaryBatchKey different response structure', async () => { const config = { resources: { diff --git a/src/runtimeHelpers.ts b/src/runtimeHelpers.ts index b3f5088..e507f92 100644 --- a/src/runtimeHelpers.ts +++ b/src/runtimeHelpers.ts @@ -125,10 +125,10 @@ export function partitionItems( export function getBatchKeysForPartitionItems( batchKey: string, ignoreKeys: Array, - items: ReadonlyArray, -): ReadonlyArray> { + items: ReadonlyArray, +): ReadonlyArray> { const groups: { - [key: string]: Array; + [key: string]: Array; } = {}; items.forEach((item, i) => { const hash = objectHash(_.omit(item, ignoreKeys), { algorithm: 'passthrough' }); @@ -305,7 +305,7 @@ export function unPartitionResults( * Example * ```js * unPartitionResultsByBatchKeyPartition( - * 'bard_id', + * 'bar_id', * [ ['name', 'rating'], ['rating'] ], * [ [2, 2], [1] ], * [ [0, 2], [1] ], @@ -331,7 +331,7 @@ export function unPartitionResultsByBatchKeyPartition( /** Should be a nested array of IDs, as generated by partitionItems */ requestGroups: ReadonlyArray>, /** The results back from the service, in the same shape as groups */ - resultGroups: ReadonlyArray>, + resultGroups: ReadonlyArray>, ): ReadonlyArray { /** * e.g. with our inputs, produce: @@ -347,33 +347,51 @@ export function unPartitionResultsByBatchKeyPartition( * ] * ``` */ - const zippedGroups = requestGroups.map(function (ids, i) { - return ids.map(function (id, j) { - for (const element of Object.values(resultGroups)[i]) { - // case 1, newKey has its own key-value pair in the response, - // { bar_id: 2, name: 'Burger King', rating: 3 } - if (Object.values(element).includes(batchKeyPartition[i][j])) { - const updatedElement = Object.assign( - {}, - ...[newKey, secondaryBatchKeyPartion[i][j]].map((key) => ({ [key]: element[key] })), - ); - return { order: id, result: updatedElement }; + + const zippedGroups: ReadonlyArray> = requestGroups.map( + (ids, i) => { + return ids.map((id, j) => { + let result = null; + for (const element of Object.values(resultGroups)[i]) { + // case 1, error in the result. + if (element instanceof CaughtResourceError) { + result = element; + break; + } + // case 2, newKey has its own key-value pair in the response. + // { bar_id: 2, name: 'Burger King', rating: 3 } + if (Object.values(element).includes(batchKeyPartition[i][j])) { + result = Object.assign( + {}, + ...[newKey, secondaryBatchKeyPartion[i][j]].map((key) => ({ [key]: element[key] })), + ); + break; + } + // case 3, the value of newKey is the key and its properties are its values. + // { 2: { name: 'Burger King', rating: 3 }} + if (batchKeyPartition[i][j] in element) { + result = { + [batchKeyPartition[i][j]]: { + [secondaryBatchKeyPartion[i][j]]: + element[batchKeyPartition[i][j]][secondaryBatchKeyPartion[i][j]], + }, + }; + break; + } } - // case 2, the value of newKey is the key and its properties are its values. - // { 2: { name: 'Burger King', rating: 3 }} - else if (batchKeyPartition[i][j] in element) { - const updatedElement = { - [batchKeyPartition[i][j]]: { - [secondaryBatchKeyPartion[i][j]]: - element[batchKeyPartition[i][j]][secondaryBatchKeyPartion[i][j]], - }, + if (result === null) { + return { + order: id, + result: new BatchItemNotFoundError( + `Could not find key = "${batchKeyPartition[i][j]}" in the response dict.`, + ), }; - // const updatedElement = Object.assign({}, ...[newKey, secondaryBatchKeyPartion[i][j]].map(key => ({[key]: element[key]}))) - return { order: id, result: updatedElement }; + } else { + return { order: id, result: result }; } - } - }); - }); + }); + }, + ); /** * Flatten and sort the groups - e.g.: * ```js From 0a393f7eadcb078da58a76ea0bef6bc1fbee4e2c Mon Sep 17 00:00:00 2001 From: Yue Guo Date: Wed, 19 May 2021 16:46:21 -0700 Subject: [PATCH 11/24] add cases that secondaryBatchKey is returned in a nested object --- __tests__/implementation.test.js | 61 +++++++++++++++++++++++++++++++- src/implementation.ts | 6 ++-- src/runtimeHelpers.ts | 37 ++++++++++++++----- 3 files changed, 91 insertions(+), 13 deletions(-) diff --git a/__tests__/implementation.test.js b/__tests__/implementation.test.js index 1a057aa..7129860 100644 --- a/__tests__/implementation.test.js +++ b/__tests__/implementation.test.js @@ -347,6 +347,62 @@ test('batch endpoint (multiple requests) with secondaryBatchKey', async () => { }); }); +test('batch endpoint (multiple requests) with secondaryBatchKey returned in a nested object ', async () => { + const config = { + resources: { + foo: { + isBatchResource: true, + docsLink: 'example.com/docs/bar', + batchKey: 'foo_ids', + newKey: 'foo_id', + secondaryBatchKey: 'properties', + secondaryNewKey: 'property', + }, + }, + }; + + const resources = { + foo: ({ foo_ids, properties, include_extra_info }) => { + if (_.isEqual(foo_ids, [2, 1])) { + expect(include_extra_info).toBe(false); + return Promise.resolve([ + { foo_id: 1, properties: { rating: 3, name: 'Burger King' } }, + { foo_id: 2, properties: { rating: 4, name: 'In N Out' } }, + ]); + } + + if (_.isEqual(foo_ids, [3])) { + expect(include_extra_info).toBe(true); + return Promise.resolve([ + { + foo_id: 3, + properties: { + rating: 5, + name: 'Shake Shack', + }, + }, + ]); + } + }, + }; + + await createDataLoaders(config, async (getLoaders) => { + const loaders = getLoaders(resources); + + const results = await loaders.foo.loadMany([ + { foo_id: 2, property: 'name', include_extra_info: false }, + { foo_id: 1, property: 'rating', include_extra_info: false }, + { foo_id: 3, property: 'rating', include_extra_info: true }, + ]); + + expect(results).toEqual([ + { foo_id: 2, properties: { name: 'In N Out' } }, + { foo_id: 1, properties: { rating: 3 } }, + { foo_id: 3, properties: { rating: 5 } }, + ]); + }); +}); + test('batch endpoint (multiple requests) with secondaryBatchKey that rejects', async () => { const config = { resources: { @@ -531,7 +587,10 @@ test('batch endpoint with secondaryBatchKey without reorderResultsByKey throws e expect(results).toMatchObject([ { foo_id: 1, name: 'Shake Shack' }, - expect.toBeError('Could not find key = "2" in the response dict.', 'BatchItemNotFoundError'), + expect.toBeError( + 'Could not find key = "2" in the response dict. Or your response does not follow the type we support.', + 'BatchItemNotFoundError', + ), { foo_id: 3, rating: 3 }, { foo_id: 4, rating: 3.5 }, ]); diff --git a/src/implementation.ts b/src/implementation.ts index 0526014..609cf7f 100644 --- a/src/implementation.ts +++ b/src/implementation.ts @@ -433,9 +433,9 @@ function getBatchLoader(resourceConfig: BatchResourceConfig, resourcePath: Reado resourceConfig.secondaryNewKey }', ['${resourceConfig.newKey}', '${resourceConfig.secondaryNewKey}'], keys); // Split the results back up into the order that they were requested - return unPartitionResultsByBatchKeyPartition('${ - resourceConfig.newKey - }', batchKeyPartition, secondaryBatchKeyPartiion, requestGroups, groupedResults); + return unPartitionResultsByBatchKeyPartition('${resourceConfig.newKey}', '${ + resourceConfig.secondaryBatchKey + }', batchKeyPartition, secondaryBatchKeyPartiion, requestGroups, groupedResults); } else { // Split the results back up into the order that they were requested return unPartitionResults(requestGroups, groupedResults); diff --git a/src/runtimeHelpers.ts b/src/runtimeHelpers.ts index e507f92..55306c6 100644 --- a/src/runtimeHelpers.ts +++ b/src/runtimeHelpers.ts @@ -326,6 +326,7 @@ export function unPartitionResults( */ export function unPartitionResultsByBatchKeyPartition( newKey: string, + secondaryBatchKey: string, batchKeyPartition: ReadonlyArray>, secondaryBatchKeyPartion: ReadonlyArray>, /** Should be a nested array of IDs, as generated by partitionItems */ @@ -358,18 +359,36 @@ export function unPartitionResultsByBatchKeyPartition( result = element; break; } - // case 2, newKey has its own key-value pair in the response. - // { bar_id: 2, name: 'Burger King', rating: 3 } + // newKey has its own key-value pair at the response top level. if (Object.values(element).includes(batchKeyPartition[i][j])) { - result = Object.assign( - {}, - ...[newKey, secondaryBatchKeyPartion[i][j]].map((key) => ({ [key]: element[key] })), - ); + // case 2, secondaryBatchKey is returned in a nested object + // { bar_id: 2, properties: {name: 'Burger King', rating: 3 }} + if ( + secondaryBatchKey in element && + secondaryBatchKeyPartion[i][j] in element[secondaryBatchKey] + ) { + result = element; + result[secondaryBatchKey] = { + [secondaryBatchKeyPartion[i][j]]: + element[secondaryBatchKey][secondaryBatchKeyPartion[i][j]], + }; + } + // case 3, secondaryBatchKey is not returned in a nested object, but also at the top level. + // { bar_id: 2, name: 'Burger King', rating: 3 } + else { + result = Object.assign( + {}, + ...[newKey, secondaryBatchKeyPartion[i][j]].map((key) => ({ [key]: element[key] })), + ); + } break; } - // case 3, the value of newKey is the key and its properties are its values. + // case 4, the value of newKey is the key and its properties are its values. // { 2: { name: 'Burger King', rating: 3 }} - if (batchKeyPartition[i][j] in element) { + if ( + batchKeyPartition[i][j] in element && + secondaryBatchKeyPartion[i][j] in element[batchKeyPartition[i][j]] + ) { result = { [batchKeyPartition[i][j]]: { [secondaryBatchKeyPartion[i][j]]: @@ -383,7 +402,7 @@ export function unPartitionResultsByBatchKeyPartition( return { order: id, result: new BatchItemNotFoundError( - `Could not find key = "${batchKeyPartition[i][j]}" in the response dict.`, + `Could not find key = "${batchKeyPartition[i][j]}" in the response dict. Or your response does not follow the type we support.`, ), }; } else { From 3fb6e34275df94f4074acf43c84dd465e5cc6e74 Mon Sep 17 00:00:00 2001 From: Yue Guo Date: Thu, 20 May 2021 10:37:09 -0700 Subject: [PATCH 12/24] change to propertyBatchKey and propertyNewKey --- __tests__/implementation.test.js | 36 +++++++++++++-------------- src/config.ts | 4 +-- src/implementation.ts | 42 ++++++++++++++++---------------- src/runtimeHelpers.ts | 35 ++++++++++++-------------- 4 files changed, 57 insertions(+), 60 deletions(-) diff --git a/__tests__/implementation.test.js b/__tests__/implementation.test.js index 7129860..438ba43 100644 --- a/__tests__/implementation.test.js +++ b/__tests__/implementation.test.js @@ -292,7 +292,7 @@ test('batch endpoint (multiple requests)', async () => { }); }); -test('batch endpoint (multiple requests) with secondaryBatchKey', async () => { +test('batch endpoint (multiple requests) with propertyBatchKey', async () => { const config = { resources: { foo: { @@ -300,8 +300,8 @@ test('batch endpoint (multiple requests) with secondaryBatchKey', async () => { docsLink: 'example.com/docs/bar', batchKey: 'foo_ids', newKey: 'foo_id', - secondaryBatchKey: 'properties', - secondaryNewKey: 'property', + propertyBatchKey: 'properties', + propertyNewKey: 'property', }, }, }; @@ -347,7 +347,7 @@ test('batch endpoint (multiple requests) with secondaryBatchKey', async () => { }); }); -test('batch endpoint (multiple requests) with secondaryBatchKey returned in a nested object ', async () => { +test('batch endpoint (multiple requests) with propertyBatchKey returned in a nested object ', async () => { const config = { resources: { foo: { @@ -355,8 +355,8 @@ test('batch endpoint (multiple requests) with secondaryBatchKey returned in a ne docsLink: 'example.com/docs/bar', batchKey: 'foo_ids', newKey: 'foo_id', - secondaryBatchKey: 'properties', - secondaryNewKey: 'property', + propertyBatchKey: 'properties', + propertyNewKey: 'property', }, }, }; @@ -403,7 +403,7 @@ test('batch endpoint (multiple requests) with secondaryBatchKey returned in a ne }); }); -test('batch endpoint (multiple requests) with secondaryBatchKey that rejects', async () => { +test('batch endpoint (multiple requests) with propertyBatchKey that rejects', async () => { const config = { resources: { foo: { @@ -411,8 +411,8 @@ test('batch endpoint (multiple requests) with secondaryBatchKey that rejects', a docsLink: 'example.com/docs/bar', batchKey: 'foo_ids', newKey: 'foo_id', - secondaryBatchKey: 'properties', - secondaryNewKey: 'property', + propertyBatchKey: 'properties', + propertyNewKey: 'property', }, }, }; @@ -468,7 +468,7 @@ test('batch endpoint (multiple requests) with secondaryBatchKey that rejects', a }); }); -test('batch endpoint (multiple requests) with secondaryBatchKey error handling', async () => { +test('batch endpoint (multiple requests) with propertyBatchKey error handling', async () => { const config = { resources: { foo: { @@ -476,8 +476,8 @@ test('batch endpoint (multiple requests) with secondaryBatchKey error handling', docsLink: 'example.com/docs/bar', batchKey: 'foo_ids', newKey: 'foo_id', - secondaryBatchKey: 'properties', - secondaryNewKey: 'property', + propertyBatchKey: 'properties', + propertyNewKey: 'property', }, }, }; @@ -533,7 +533,7 @@ test('batch endpoint (multiple requests) with secondaryBatchKey error handling', }); }); -test('batch endpoint with secondaryBatchKey without reorderResultsByKey throws error for response with non existant items', async () => { +test('batch endpoint with propertyBatchKey without reorderResultsByKey throws error for response with non existant items', async () => { const config = { resources: { foo: { @@ -541,8 +541,8 @@ test('batch endpoint with secondaryBatchKey without reorderResultsByKey throws e docsLink: 'example.com/docs/bar', batchKey: 'foo_ids', newKey: 'foo_id', - secondaryBatchKey: 'properties', - secondaryNewKey: 'property', + propertyBatchKey: 'properties', + propertyNewKey: 'property', }, }, }; @@ -597,7 +597,7 @@ test('batch endpoint with secondaryBatchKey without reorderResultsByKey throws e }); }); -test('batch endpoint (multiple requests) with secondaryBatchKey different response structure', async () => { +test('batch endpoint (multiple requests) with propertyBatchKey different response structure', async () => { const config = { resources: { foo: { @@ -605,8 +605,8 @@ test('batch endpoint (multiple requests) with secondaryBatchKey different respon docsLink: 'example.com/docs/bar', batchKey: 'foo_ids', newKey: 'foo_id', - secondaryBatchKey: 'properties', - secondaryNewKey: 'property', + propertyBatchKey: 'properties', + propertyNewKey: 'property', }, }, }; diff --git a/src/config.ts b/src/config.ts index 8046984..dedc73a 100644 --- a/src/config.ts +++ b/src/config.ts @@ -20,8 +20,8 @@ export interface BatchResourceConfig { isBatchResource: true; batchKey: string; newKey: string; - secondaryBatchKey: string; - secondaryNewKey: string; + propertyBatchKey: string; + propertyNewKey: string; reorderResultsByKey?: string; nestedPath?: string; commaSeparatedBatchKey?: boolean; diff --git a/src/implementation.ts b/src/implementation.ts index 609cf7f..1acf9f5 100644 --- a/src/implementation.ts +++ b/src/implementation.ts @@ -166,10 +166,10 @@ function getBatchLoader(resourceConfig: BatchResourceConfig, resourcePath: Reado * We'll refer to each element in the group as a "request ID". */ let requestGroups; - if (${typeof resourceConfig.secondaryNewKey === 'string'} && ${ - typeof resourceConfig.secondaryBatchKey === 'string' + if (${typeof resourceConfig.propertyNewKey === 'string'} && ${ + typeof resourceConfig.propertyBatchKey === 'string' }) { - requestGroups = partitionItems(['${resourceConfig.newKey}', '${resourceConfig.secondaryNewKey}'], keys); + requestGroups = partitionItems(['${resourceConfig.newKey}', '${resourceConfig.propertyNewKey}'], keys); } else { requestGroups = partitionItems('${resourceConfig.newKey}', keys); } @@ -189,8 +189,8 @@ function getBatchLoader(resourceConfig: BatchResourceConfig, resourcePath: Reado const { batchKey, newKey, - secondaryBatchKey, - secondaryNewKey, + propertyBatchKey, + propertyNewKey, commaSeparatedBatchKey, } = resourceConfig; @@ -199,18 +199,18 @@ function getBatchLoader(resourceConfig: BatchResourceConfig, resourcePath: Reado batchKeyParam = `${batchKeyParam}.join(',')`; } - if (typeof secondaryNewKey === 'string' && typeof secondaryBatchKey === 'string') { - let secondaryBatchKeyParam = `['${secondaryBatchKey}']: requests.map(k => k['${secondaryNewKey}'])`; + if (typeof propertyNewKey === 'string' && typeof propertyBatchKey === 'string') { + let propertyBatchKeyParam = `['${propertyBatchKey}']: requests.map(k => k['${propertyNewKey}'])`; if (commaSeparatedBatchKey === true) { - secondaryBatchKeyParam = `${secondaryBatchKeyParam}.join(',')`; + propertyBatchKeyParam = `${propertyBatchKeyParam}.join(',')`; } return ` // For now, we assume that the dataloader key should be the first argument to the resource // @see https://github.com/Yelp/dataloader-codegen/issues/56 const resourceArgs = [{ - ..._.omit(requests[0], '${resourceConfig.newKey}', '${resourceConfig.secondaryNewKey}'), + ..._.omit(requests[0], '${resourceConfig.newKey}', '${resourceConfig.propertyNewKey}'), ${batchKeyParam}, - ${secondaryBatchKeyParam}, + ${propertyBatchKeyParam}, }]; `; } else { @@ -303,14 +303,14 @@ function getBatchLoader(resourceConfig: BatchResourceConfig, resourcePath: Reado const { reorderResultsByKey, isResponseDictionary, - secondaryBatchKey, - secondaryNewKey, + propertyBatchKey, + propertyNewKey, } = resourceConfig; if ( !isResponseDictionary && reorderResultsByKey == null && - !(typeof secondaryNewKey === 'string' && typeof secondaryBatchKey === 'string') + !(typeof propertyNewKey === 'string' && typeof propertyBatchKey === 'string') ) { return ` if (!(response instanceof Error)) { @@ -423,19 +423,19 @@ function getBatchLoader(resourceConfig: BatchResourceConfig, resourcePath: Reado return response; })) - if (${typeof resourceConfig.secondaryNewKey === 'string'} && ${ - typeof resourceConfig.secondaryBatchKey === 'string' + if (${typeof resourceConfig.propertyNewKey === 'string'} && ${ + typeof resourceConfig.propertyBatchKey === 'string' }) { const batchKeyPartition = getBatchKeysForPartitionItems('${resourceConfig.newKey}', ['${ resourceConfig.newKey - }', '${resourceConfig.secondaryNewKey}'], keys); - const secondaryBatchKeyPartiion = getBatchKeysForPartitionItems('${ - resourceConfig.secondaryNewKey - }', ['${resourceConfig.newKey}', '${resourceConfig.secondaryNewKey}'], keys); + }', '${resourceConfig.propertyNewKey}'], keys); + const propertyBatchKeyPartiion = getBatchKeysForPartitionItems('${resourceConfig.propertyNewKey}', ['${ + resourceConfig.newKey + }', '${resourceConfig.propertyNewKey}'], keys); // Split the results back up into the order that they were requested return unPartitionResultsByBatchKeyPartition('${resourceConfig.newKey}', '${ - resourceConfig.secondaryBatchKey - }', batchKeyPartition, secondaryBatchKeyPartiion, requestGroups, groupedResults); + resourceConfig.propertyBatchKey + }', batchKeyPartition, propertyBatchKeyPartiion, requestGroups, groupedResults); } else { // Split the results back up into the order that they were requested return unPartitionResults(requestGroups, groupedResults); diff --git a/src/runtimeHelpers.ts b/src/runtimeHelpers.ts index 55306c6..b76a3f8 100644 --- a/src/runtimeHelpers.ts +++ b/src/runtimeHelpers.ts @@ -62,7 +62,7 @@ export const cacheKeyOptions = { /** * Take in all objects passed to .load(), and bucket them by the non - * batch keys (i.e. `batchKey` and `secondaryBatchKey`) attributes. + * batch keys (i.e. `batchKey` and `propertyBatchKey`) attributes. * * We use this to chunk up the requests to the resource. * @@ -99,10 +99,10 @@ export function partitionItems( /** * Take in all objects passed to .load(), and bucket them by the non - * batch keys (i.e. `batchKey` and `secondaryBatchKey`) attributes. + * batch keys (i.e. `batchKey` and `propertyBatchKey`) attributes. * Return batch keys value for each partition items. * - * This function is only called when we have secondaryBatchKey, and it's + * This function is only called when we have propertyBatchKey, and it's * used to map result to the order of requests. * * Example: @@ -299,7 +299,7 @@ export function unPartitionResults( /** * Perform the inverse mapping from partitionItems on the nested results we get - * back from the service. This function is only called when we have secondaryBatchKey. + * back from the service. This function is only called when we have propertyBatchKey. * We assume the newKey is inside the response here. * * Example @@ -326,9 +326,9 @@ export function unPartitionResults( */ export function unPartitionResultsByBatchKeyPartition( newKey: string, - secondaryBatchKey: string, + propertyBatchKey: string, batchKeyPartition: ReadonlyArray>, - secondaryBatchKeyPartion: ReadonlyArray>, + propertyBatchKeyPartion: ReadonlyArray>, /** Should be a nested array of IDs, as generated by partitionItems */ requestGroups: ReadonlyArray>, /** The results back from the service, in the same shape as groups */ @@ -361,24 +361,21 @@ export function unPartitionResultsByBatchKeyPartition( } // newKey has its own key-value pair at the response top level. if (Object.values(element).includes(batchKeyPartition[i][j])) { - // case 2, secondaryBatchKey is returned in a nested object + // case 2, propertyBatchKey is returned in a nested object // { bar_id: 2, properties: {name: 'Burger King', rating: 3 }} - if ( - secondaryBatchKey in element && - secondaryBatchKeyPartion[i][j] in element[secondaryBatchKey] - ) { + if (propertyBatchKey in element && propertyBatchKeyPartion[i][j] in element[propertyBatchKey]) { result = element; - result[secondaryBatchKey] = { - [secondaryBatchKeyPartion[i][j]]: - element[secondaryBatchKey][secondaryBatchKeyPartion[i][j]], + result[propertyBatchKey] = { + [propertyBatchKeyPartion[i][j]]: + element[propertyBatchKey][propertyBatchKeyPartion[i][j]], }; } - // case 3, secondaryBatchKey is not returned in a nested object, but also at the top level. + // case 3, propertyBatchKey is not returned in a nested object, but also at the top level. // { bar_id: 2, name: 'Burger King', rating: 3 } else { result = Object.assign( {}, - ...[newKey, secondaryBatchKeyPartion[i][j]].map((key) => ({ [key]: element[key] })), + ...[newKey, propertyBatchKeyPartion[i][j]].map((key) => ({ [key]: element[key] })), ); } break; @@ -387,12 +384,12 @@ export function unPartitionResultsByBatchKeyPartition( // { 2: { name: 'Burger King', rating: 3 }} if ( batchKeyPartition[i][j] in element && - secondaryBatchKeyPartion[i][j] in element[batchKeyPartition[i][j]] + propertyBatchKeyPartion[i][j] in element[batchKeyPartition[i][j]] ) { result = { [batchKeyPartition[i][j]]: { - [secondaryBatchKeyPartion[i][j]]: - element[batchKeyPartition[i][j]][secondaryBatchKeyPartion[i][j]], + [propertyBatchKeyPartion[i][j]]: + element[batchKeyPartition[i][j]][propertyBatchKeyPartion[i][j]], }, }; break; From a5e440f210c9c2ce796870efa4f8e8439cdb7b95 Mon Sep 17 00:00:00 2001 From: Yue Guo Date: Thu, 20 May 2021 10:43:07 -0700 Subject: [PATCH 13/24] add propertyBatchKey and propertyNewKey to schema.json --- schema.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/schema.json b/schema.json index c4bfe37..5337007 100644 --- a/schema.json +++ b/schema.json @@ -97,6 +97,14 @@ "isBatchKeyASet": { "type": "boolean", "description": "(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": { + "type": "string", + "description": "The argument to the resource that represents the list of properties we want to fetch. (e.g. 'properties', 'features')" + }, + "propertyNewKey": { + "type": "string", + "description": "The argument we'll replace the propertyBatchKey with - should be a singular version of the batchKey (e.g. 'property', 'feature')" } } }, From 5d2b19d72e20a2e9dba2eecebbaa3f3379164529 Mon Sep 17 00:00:00 2001 From: Yue Guo Date: Thu, 20 May 2021 16:51:58 -0700 Subject: [PATCH 14/24] add more tests to prove this work with other existing config --- __tests__/implementation.test.js | 1452 ++++++++++++++++-------------- src/runtimeHelpers.ts | 1 + 2 files changed, 792 insertions(+), 661 deletions(-) diff --git a/__tests__/implementation.test.js b/__tests__/implementation.test.js index 4cd60c7..ee1d013 100644 --- a/__tests__/implementation.test.js +++ b/__tests__/implementation.test.js @@ -292,7 +292,7 @@ test('batch endpoint (multiple requests)', async () => { }); }); -test('batch endpoint (multiple requests) with propertyBatchKey', async () => { +test('batch endpoint that rejects', async () => { const config = { resources: { foo: { @@ -300,29 +300,33 @@ test('batch endpoint (multiple requests) with propertyBatchKey', async () => { docsLink: 'example.com/docs/bar', batchKey: 'foo_ids', newKey: 'foo_id', - propertyBatchKey: 'properties', - propertyNewKey: 'property', }, }, }; const resources = { - foo: ({ foo_ids, properties, include_extra_info }) => { - if (_.isEqual(foo_ids, [2, 1])) { + foo: ({ foo_ids, include_extra_info }) => { + if (_.isEqual(foo_ids, [1, 3])) { expect(include_extra_info).toBe(false); - return Promise.resolve([ - { foo_id: 1, rating: 3, name: 'Burger King' }, - { foo_id: 2, rating: 4, name: 'In N Out' }, - ]); + return Promise.reject('yikes'); } - if (_.isEqual(foo_ids, [3])) { + if (_.isEqual(foo_ids, [2, 4, 5])) { expect(include_extra_info).toBe(true); return Promise.resolve([ { - foo_id: 3, - rating: 5, - name: 'Shake Shack', + foo_id: 2, + foo_value: 'greetings', + extra_stuff: 'lorem ipsum', + }, + { + foo_id: 4, + foo_value: 'greetings', + extra_stuff: 'lorem ipsum', + }, + { + foo_id: 5, + foo_value: 'greetings', extra_stuff: 'lorem ipsum', }, ]); @@ -334,20 +338,24 @@ test('batch endpoint (multiple requests) with propertyBatchKey', async () => { const loaders = getLoaders(resources); const results = await loaders.foo.loadMany([ - { foo_id: 2, property: 'name', include_extra_info: false }, - { foo_id: 1, property: 'rating', include_extra_info: false }, - { foo_id: 3, property: 'rating', include_extra_info: true }, + { foo_id: 1, include_extra_info: false }, + { foo_id: 2, include_extra_info: true }, + { foo_id: 3, include_extra_info: false }, + { foo_id: 4, include_extra_info: true }, + { foo_id: 5, include_extra_info: true }, ]); - - expect(results).toEqual([ - { foo_id: 2, name: 'In N Out' }, - { foo_id: 1, rating: 3 }, - { foo_id: 3, rating: 5 }, + // NonError comes from the default error handler which uses ensure-error + expect(results).toMatchObject([ + expect.toBeError(/yikes/, 'NonError'), + { foo_id: 2, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, + expect.toBeError(/yikes/, 'NonError'), + { foo_id: 4, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, + { foo_id: 5, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, ]); }); }); -test('batch endpoint (multiple requests) with propertyBatchKey returned in a nested object ', async () => { +test('batch endpoint (multiple requests, default error handling)', async () => { const config = { resources: { foo: { @@ -355,31 +363,34 @@ test('batch endpoint (multiple requests) with propertyBatchKey returned in a nes docsLink: 'example.com/docs/bar', batchKey: 'foo_ids', newKey: 'foo_id', - propertyBatchKey: 'properties', - propertyNewKey: 'property', }, }, }; const resources = { - foo: ({ foo_ids, properties, include_extra_info }) => { - if (_.isEqual(foo_ids, [2, 1])) { + foo: ({ foo_ids, include_extra_info }) => { + if (_.isEqual(foo_ids, [1, 3])) { expect(include_extra_info).toBe(false); - return Promise.resolve([ - { foo_id: 1, properties: { rating: 3, name: 'Burger King' } }, - { foo_id: 2, properties: { rating: 4, name: 'In N Out' } }, - ]); + throw new Error('yikes'); } - if (_.isEqual(foo_ids, [3])) { + if (_.isEqual(foo_ids, [2, 4, 5])) { expect(include_extra_info).toBe(true); return Promise.resolve([ { - foo_id: 3, - properties: { - rating: 5, - name: 'Shake Shack', - }, + foo_id: 2, + foo_value: 'greetings', + extra_stuff: 'lorem ipsum', + }, + { + foo_id: 4, + foo_value: 'greetings', + extra_stuff: 'lorem ipsum', + }, + { + foo_id: 5, + foo_value: 'greetings', + extra_stuff: 'lorem ipsum', }, ]); } @@ -390,20 +401,30 @@ test('batch endpoint (multiple requests) with propertyBatchKey returned in a nes const loaders = getLoaders(resources); const results = await loaders.foo.loadMany([ - { foo_id: 2, property: 'name', include_extra_info: false }, - { foo_id: 1, property: 'rating', include_extra_info: false }, - { foo_id: 3, property: 'rating', include_extra_info: true }, + { foo_id: 1, include_extra_info: false }, + { foo_id: 2, include_extra_info: true }, + { foo_id: 3, include_extra_info: false }, + { foo_id: 4, include_extra_info: true }, + { foo_id: 5, include_extra_info: true }, ]); - expect(results).toEqual([ - { foo_id: 2, properties: { name: 'In N Out' } }, - { foo_id: 1, properties: { rating: 3 } }, - { foo_id: 3, properties: { rating: 5 } }, + expect(results).toMatchObject([ + expect.toBeError(/yikes/), + { foo_id: 2, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, + expect.toBeError(/yikes/), + { foo_id: 4, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, + { foo_id: 5, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, ]); }); }); -test('batch endpoint (multiple requests) with propertyBatchKey that rejects', async () => { +test('batch endpoint (multiple requests, custom error handling)', async () => { + async function errorHandler(resourcePath, error) { + expect(resourcePath).toEqual(['foo']); + expect(error.message).toBe('yikes'); + return new Error('hello from custom error handler'); + } + const config = { resources: { foo: { @@ -411,35 +432,33 @@ test('batch endpoint (multiple requests) with propertyBatchKey that rejects', as docsLink: 'example.com/docs/bar', batchKey: 'foo_ids', newKey: 'foo_id', - propertyBatchKey: 'properties', - propertyNewKey: 'property', }, }, }; const resources = { - foo: ({ foo_ids, properties, include_extra_info }) => { + foo: ({ foo_ids, include_extra_info }) => { if (_.isEqual(foo_ids, [1, 3])) { expect(include_extra_info).toBe(false); - return Promise.reject('yikes'); + throw new Error('yikes'); } + if (_.isEqual(foo_ids, [2, 4, 5])) { expect(include_extra_info).toBe(true); return Promise.resolve([ { foo_id: 2, - name: 'Burger King', - rating: 3, + foo_value: 'greetings', + extra_stuff: 'lorem ipsum', }, { foo_id: 4, - name: 'In N Out', - rating: 3.5, + foo_value: 'greetings', + extra_stuff: 'lorem ipsum', }, { foo_id: 5, - name: 'Shake Shack', - rating: 4, + foo_value: 'greetings', extra_stuff: 'lorem ipsum', }, ]); @@ -448,66 +467,68 @@ test('batch endpoint (multiple requests) with propertyBatchKey that rejects', as }; await createDataLoaders(config, async (getLoaders) => { - const loaders = getLoaders(resources); + const loaders = getLoaders(resources, { errorHandler }); const results = await loaders.foo.loadMany([ - { foo_id: 1, property: 'name', include_extra_info: false }, - { foo_id: 2, property: 'rating', include_extra_info: true }, - { foo_id: 3, property: 'name', include_extra_info: false }, - { foo_id: 4, property: 'rating', include_extra_info: true }, - { foo_id: 5, property: 'name', include_extra_info: true }, + { foo_id: 1, include_extra_info: false }, + { foo_id: 2, include_extra_info: true }, + { foo_id: 3, include_extra_info: false }, + { foo_id: 4, include_extra_info: true }, + { foo_id: 5, include_extra_info: true }, ]); expect(results).toMatchObject([ - expect.toBeError(/yikes/, 'NonError'), - { foo_id: 2, rating: 3 }, - expect.toBeError(/yikes/, 'NonError'), - { foo_id: 4, rating: 3.5 }, - { foo_id: 5, name: 'Shake Shack' }, + expect.toBeError(/hello from custom error handler/), + { foo_id: 2, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, + expect.toBeError(/hello from custom error handler/), + { foo_id: 4, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, + { foo_id: 5, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, ]); }); }); -test('batch endpoint (multiple requests) with propertyBatchKey error handling', async () => { +test('batch endpoint (multiple requests, error handling, nestedPath)', async () => { const config = { + eek: true, resources: { foo: { isBatchResource: true, docsLink: 'example.com/docs/bar', batchKey: 'foo_ids', newKey: 'foo_id', - propertyBatchKey: 'properties', - propertyNewKey: 'property', + nestedPath: 'foo_data', }, }, }; 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([ - { - foo_id: 2, - name: 'Burger King', - rating: 3, - }, - { - foo_id: 4, - name: 'In N Out', - rating: 3.5, - }, - { - foo_id: 5, - name: 'Shake Shack', - rating: 4, - extra_stuff: 'lorem ipsum', - }, - ]); - } + foo: ({ foo_ids, include_extra_info }) => { if (_.isEqual(foo_ids, [1, 3])) { expect(include_extra_info).toBe(false); - throw new Error('yikes'); + return new Error('yikes'); + } + + if (_.isEqual(foo_ids, [2, 4, 5])) { + expect(include_extra_info).toBe(true); + return Promise.resolve({ + foo_data: [ + { + foo_id: 2, + foo_value: 'greetings', + extra_stuff: 'lorem ipsum', + }, + { + foo_id: 4, + foo_value: 'greetings', + extra_stuff: 'lorem ipsum', + }, + { + foo_id: 5, + foo_value: 'greetings', + extra_stuff: 'lorem ipsum', + }, + ], + }); } }, }; @@ -516,121 +537,49 @@ test('batch endpoint (multiple requests) with propertyBatchKey error handling', const loaders = getLoaders(resources); const results = await loaders.foo.loadMany([ - { foo_id: 1, property: 'name', include_extra_info: false }, - { foo_id: 2, property: 'rating', include_extra_info: true }, - { foo_id: 3, property: 'name', include_extra_info: false }, - { foo_id: 4, property: 'rating', include_extra_info: true }, - { foo_id: 5, property: 'name', include_extra_info: true }, + { foo_id: 1, include_extra_info: false }, + { foo_id: 2, include_extra_info: true }, + { foo_id: 3, include_extra_info: false }, + { foo_id: 4, include_extra_info: true }, + { foo_id: 5, include_extra_info: true }, ]); expect(results).toMatchObject([ expect.toBeError(/yikes/), - { foo_id: 2, rating: 3 }, + { foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, expect.toBeError(/yikes/), - { foo_id: 4, rating: 3.5 }, - { foo_id: 5, name: 'Shake Shack' }, + { foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, + { foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, ]); }); }); -test('batch endpoint with propertyBatchKey without reorderResultsByKey throws error for response with non existant items', async () => { +test('batch endpoint (multiple requests, error handling - non array response)', async () => { const config = { + eek: true, resources: { foo: { isBatchResource: true, docsLink: 'example.com/docs/bar', batchKey: 'foo_ids', newKey: 'foo_id', - propertyBatchKey: 'properties', - propertyNewKey: 'property', }, }, }; const resources = { - foo: ({ foo_ids, bar }) => { - if (_.isEqual(foo_ids, [1, 2, 3])) { - 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])) { - return Promise.resolve([ - { - foo_id: 4, - name: 'In N Out', - rating: 3.5, - }, - ]); + foo: ({ foo_ids, include_extra_info }) => { + if (_.isEqual(foo_ids, [1, 3])) { + expect(include_extra_info).toBe(false); + return new Error('yikes'); } - }, - }; - - await createDataLoaders(config, async (getLoaders) => { - const loaders = getLoaders(resources); - const results = await loaders.foo.loadMany([ - { foo_id: 1, property: 'name', include_extra_info: true }, - { foo_id: 2, property: 'rating', include_extra_info: true }, - { foo_id: 3, property: 'rating', include_extra_info: true }, - { foo_id: 4, property: 'rating', include_extra_info: false }, - ]); - - expect(results).toMatchObject([ - { foo_id: 1, name: 'Shake Shack' }, - expect.toBeError( - 'Could not find key = "2" in the response dict. Or your response does not follow the type we support.', - 'BatchItemNotFoundError', - ), - { foo_id: 3, rating: 3 }, - { foo_id: 4, rating: 3.5 }, - ]); - }); -}); - -test('batch endpoint (multiple requests) with propertyBatchKey different response structure', async () => { - const config = { - resources: { - foo: { - isBatchResource: true, - docsLink: 'example.com/docs/bar', - batchKey: 'foo_ids', - newKey: 'foo_id', - propertyBatchKey: 'properties', - propertyNewKey: 'property', - }, - }, - }; - - const resources = { - foo: ({ foo_ids, properties, include_extra_info }) => { - if (_.isEqual(foo_ids, [2, 1]) && _.isEqual(properties, ['name', 'rating'])) { - expect(include_extra_info).toBe(false); - return Promise.resolve([ - { 1: { rating: 3, name: 'Burger King' } }, - { 2: { rating: 4, name: 'In N Out' } }, - ]); - } - - if (_.isEqual(foo_ids, [3]) && _.isEqual(properties, ['rating'])) { + if (_.isEqual(foo_ids, [2, 4, 5])) { expect(include_extra_info).toBe(true); - return Promise.resolve([ - { - 3: { - rating: 5, - extra_stuff: 'lorem ipsum', - }, - }, - ]); + // Deliberately returning an object, not an array + return Promise.resolve({ + foo: 'bar', + }); } }, }; @@ -639,16 +588,24 @@ test('batch endpoint (multiple requests) with propertyBatchKey different respons const loaders = getLoaders(resources); const results = await loaders.foo.loadMany([ - { foo_id: 2, property: 'name', include_extra_info: false }, - { foo_id: 1, property: 'rating', include_extra_info: false }, - { foo_id: 3, property: 'rating', include_extra_info: true }, + { foo_id: 1, include_extra_info: false }, + { foo_id: 2, include_extra_info: true }, + { foo_id: 3, include_extra_info: false }, + { foo_id: 4, include_extra_info: true }, + { foo_id: 5, include_extra_info: true }, ]); - expect(results).toEqual([{ 2: { name: 'In N Out' } }, { 1: { rating: 3 } }, { 3: { rating: 5 } }]); + expect(results).toMatchObject([ + expect.toBeError('yikes'), + expect.toBeError('[dataloader-codegen :: foo] Expected response to be an array'), + expect.toBeError('yikes'), + expect.toBeError('[dataloader-codegen :: foo] Expected response to be an array'), + expect.toBeError('[dataloader-codegen :: foo] Expected response to be an array'), + ]); }); }); -test('batch endpoint that rejects', async () => { +test('batch endpoint (multiple requests, error handling, with reordering)', async () => { const config = { resources: { foo: { @@ -656,6 +613,7 @@ test('batch endpoint that rejects', async () => { docsLink: 'example.com/docs/bar', batchKey: 'foo_ids', newKey: 'foo_id', + reorderResultsByKey: 'foo_id', }, }, }; @@ -664,24 +622,25 @@ test('batch endpoint that rejects', async () => { foo: ({ foo_ids, include_extra_info }) => { if (_.isEqual(foo_ids, [1, 3])) { expect(include_extra_info).toBe(false); - return Promise.reject('yikes'); + return new Error('yikes'); } if (_.isEqual(foo_ids, [2, 4, 5])) { expect(include_extra_info).toBe(true); + // return items deliberately out of order return Promise.resolve([ { - foo_id: 2, + foo_id: 4, foo_value: 'greetings', extra_stuff: 'lorem ipsum', }, { - foo_id: 4, + foo_id: 5, foo_value: 'greetings', extra_stuff: 'lorem ipsum', }, { - foo_id: 5, + foo_id: 2, foo_value: 'greetings', extra_stuff: 'lorem ipsum', }, @@ -700,18 +659,24 @@ test('batch endpoint that rejects', async () => { { foo_id: 4, include_extra_info: true }, { foo_id: 5, include_extra_info: true }, ]); - // NonError comes from the default error handler which uses ensure-error + expect(results).toMatchObject([ - expect.toBeError(/yikes/, 'NonError'), + expect.toBeError(/yikes/), { foo_id: 2, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, - expect.toBeError(/yikes/, 'NonError'), + expect.toBeError(/yikes/), { foo_id: 4, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, { foo_id: 5, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, ]); }); }); -test('batch endpoint (multiple requests, default error handling)', async () => { +/** + * Without reorderResultsByKey: + * If we requested 3 items, but the resource only returns 2, we don't know which + * response is missing. It's unsafe to return any results, so we must throw an + * error for the whole set of requests. + */ +test('batch endpoint without reorderResultsByKey throws error for response with non existant items', async () => { const config = { resources: { foo: { @@ -724,29 +689,26 @@ test('batch endpoint (multiple requests, default error handling)', async () => { }; const resources = { - foo: ({ foo_ids, include_extra_info }) => { - if (_.isEqual(foo_ids, [1, 3])) { - expect(include_extra_info).toBe(false); - throw new Error('yikes'); - } - - if (_.isEqual(foo_ids, [2, 4, 5])) { - expect(include_extra_info).toBe(true); + foo: ({ foo_ids, bar }) => { + if (_.isEqual(foo_ids, [1, 2, 3])) { return Promise.resolve([ { - foo_id: 2, + foo_id: 1, foo_value: 'greetings', extra_stuff: 'lorem ipsum', }, + // deliberately omit 2 { - foo_id: 4, + foo_id: 3, foo_value: 'greetings', extra_stuff: 'lorem ipsum', }, + ]); + } else if (_.isEqual(foo_ids, [4])) { + return Promise.resolve([ { - foo_id: 5, + foo_id: 4, foo_value: 'greetings', - extra_stuff: 'lorem ipsum', }, ]); } @@ -757,30 +719,31 @@ test('batch endpoint (multiple requests, default error handling)', async () => { const loaders = getLoaders(resources); const results = await loaders.foo.loadMany([ - { foo_id: 1, include_extra_info: false }, + { foo_id: 1, include_extra_info: true }, { foo_id: 2, include_extra_info: true }, - { foo_id: 3, include_extra_info: false }, - { foo_id: 4, include_extra_info: true }, - { foo_id: 5, include_extra_info: true }, + { foo_id: 3, include_extra_info: true }, + { foo_id: 4, include_extra_info: false }, ]); expect(results).toMatchObject([ - expect.toBeError(/yikes/), - { foo_id: 2, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, - expect.toBeError(/yikes/), - { foo_id: 4, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, - { foo_id: 5, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, + expect.toBeError( + '[dataloader-codegen :: foo] Resource returned 2 items, but we requested 3 items', + 'BatchItemNotFoundError', + ), + expect.toBeError( + '[dataloader-codegen :: foo] Resource returned 2 items, but we requested 3 items', + 'BatchItemNotFoundError', + ), + expect.toBeError( + '[dataloader-codegen :: foo] Resource returned 2 items, but we requested 3 items', + 'BatchItemNotFoundError', + ), + { foo_id: 4, foo_value: 'greetings' }, ]); }); }); -test('batch endpoint (multiple requests, custom error handling)', async () => { - async function errorHandler(resourcePath, error) { - expect(resourcePath).toEqual(['foo']); - expect(error.message).toBe('yikes'); - return new Error('hello from custom error handler'); - } - +test('batch endpoint with reorderResultsByKey handles response with non existant items', async () => { const config = { resources: { foo: { @@ -788,32 +751,31 @@ test('batch endpoint (multiple requests, custom error handling)', async () => { docsLink: 'example.com/docs/bar', batchKey: 'foo_ids', newKey: 'foo_id', + reorderResultsByKey: 'foo_id', }, }, }; const resources = { - foo: ({ foo_ids, include_extra_info }) => { - if (_.isEqual(foo_ids, [1, 3])) { - expect(include_extra_info).toBe(false); - throw new Error('yikes'); - } - - if (_.isEqual(foo_ids, [2, 4, 5])) { - expect(include_extra_info).toBe(true); + foo: ({ foo_ids, bar }) => { + if (_.isEqual(foo_ids, [1, 2, 3])) { return Promise.resolve([ { - foo_id: 2, + foo_id: 1, foo_value: 'greetings', extra_stuff: 'lorem ipsum', }, + // deliberately omit 2 { - foo_id: 4, + foo_id: 3, foo_value: 'greetings', extra_stuff: 'lorem ipsum', }, + ]); + } else if (_.isEqual(foo_ids, [4])) { + return Promise.resolve([ { - foo_id: 5, + foo_id: 4, foo_value: 'greetings', extra_stuff: 'lorem ipsum', }, @@ -823,119 +785,95 @@ test('batch endpoint (multiple requests, custom error handling)', async () => { }; await createDataLoaders(config, async (getLoaders) => { - const loaders = getLoaders(resources, { errorHandler }); + const loaders = getLoaders(resources); const results = await loaders.foo.loadMany([ - { foo_id: 1, include_extra_info: false }, - { foo_id: 2, include_extra_info: true }, - { foo_id: 3, include_extra_info: false }, - { foo_id: 4, include_extra_info: true }, - { foo_id: 5, include_extra_info: true }, + { foo_id: 1, bar: true }, + { foo_id: 2, bar: true }, + { foo_id: 3, bar: true }, + { foo_id: 4, bar: false }, ]); expect(results).toMatchObject([ - expect.toBeError(/hello from custom error handler/), - { foo_id: 2, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, - expect.toBeError(/hello from custom error handler/), + { foo_id: 1, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, + expect.toBeError( + '[dataloader-codegen :: foo] Response did not contain item with foo_id = 2', + 'BatchItemNotFoundError', + ), + { foo_id: 3, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, { foo_id: 4, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, - { foo_id: 5, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, ]); }); }); -test('batch endpoint (multiple requests, error handling, nestedPath)', async () => { +test('batch endpoint with isResponseDictionary handles a response that returns a dictionary', async () => { const config = { - eek: true, resources: { foo: { isBatchResource: true, - docsLink: 'example.com/docs/bar', + docsLink: 'example.com/docs/foos', batchKey: 'foo_ids', newKey: 'foo_id', - nestedPath: 'foo_data', + isResponseDictionary: true, }, }, }; const resources = { - foo: ({ foo_ids, include_extra_info }) => { - if (_.isEqual(foo_ids, [1, 3])) { - expect(include_extra_info).toBe(false); - return new Error('yikes'); - } - - if (_.isEqual(foo_ids, [2, 4, 5])) { - expect(include_extra_info).toBe(true); - return Promise.resolve({ - foo_data: [ - { - foo_id: 2, - foo_value: 'greetings', - extra_stuff: 'lorem ipsum', - }, - { - foo_id: 4, - foo_value: 'greetings', - extra_stuff: 'lorem ipsum', - }, - { - foo_id: 5, - foo_value: 'greetings', - extra_stuff: 'lorem ipsum', - }, - ], - }); - } + foo: ({ foo_ids }) => { + expect(foo_ids).toEqual([1, 2, 3]); + return Promise.resolve({ + 2: { foo_id: 2, foo_value: 'world' }, + 1: { foo_id: 1, foo_value: 'hello' }, + 3: { foo_id: 3, foo_value: '!' }, + }); }, }; await createDataLoaders(config, async (getLoaders) => { const loaders = getLoaders(resources); - const results = await loaders.foo.loadMany([ - { foo_id: 1, include_extra_info: false }, - { foo_id: 2, include_extra_info: true }, - { foo_id: 3, include_extra_info: false }, - { foo_id: 4, include_extra_info: true }, - { foo_id: 5, include_extra_info: true }, - ]); - - expect(results).toMatchObject([ - expect.toBeError(/yikes/), - { foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, - expect.toBeError(/yikes/), - { foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, - { foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, + const results = await loaders.foo.loadMany([{ foo_id: 1 }, { foo_id: 2 }, { foo_id: 3 }]); + expect(results).toEqual([ + { foo_id: 1, foo_value: 'hello' }, + { foo_id: 2, foo_value: 'world' }, + { foo_id: 3, foo_value: '!' }, ]); }); }); -test('batch endpoint (multiple requests, error handling - non array response)', async () => { +test('batch endpoint with isBatchKeyASet handles a response', async () => { const config = { - eek: true, resources: { foo: { isBatchResource: true, docsLink: 'example.com/docs/bar', batchKey: 'foo_ids', newKey: 'foo_id', + isBatchKeyASet: true, }, }, }; const resources = { foo: ({ foo_ids, include_extra_info }) => { - if (_.isEqual(foo_ids, [1, 3])) { + if (_.isEqual(foo_ids, [1, 2])) { expect(include_extra_info).toBe(false); - return new Error('yikes'); + return Promise.resolve([ + { foo_id: 1, foo_value: 'hello' }, + { foo_id: 2, foo_value: 'world' }, + ]); } - if (_.isEqual(foo_ids, [2, 4, 5])) { + if (_.isEqual(foo_ids, [3])) { expect(include_extra_info).toBe(true); - // Deliberately returning an object, not an array - return Promise.resolve({ - foo: 'bar', - }); + return Promise.resolve([ + { + foo_id: 3, + foo_value: 'greetings', + extra_stuff: 'lorem ipsum', + }, + ]); } }, }; @@ -945,94 +883,80 @@ test('batch endpoint (multiple requests, error handling - non array response)', const results = await loaders.foo.loadMany([ { foo_id: 1, include_extra_info: false }, - { foo_id: 2, include_extra_info: true }, - { foo_id: 3, include_extra_info: false }, - { foo_id: 4, include_extra_info: true }, - { foo_id: 5, include_extra_info: true }, + { foo_id: 2, include_extra_info: false }, + { foo_id: 3, include_extra_info: true }, ]); - expect(results).toMatchObject([ - expect.toBeError('yikes'), - expect.toBeError('[dataloader-codegen :: foo] Expected response to be an array'), - expect.toBeError('yikes'), - expect.toBeError('[dataloader-codegen :: foo] Expected response to be an array'), - expect.toBeError('[dataloader-codegen :: foo] Expected response to be an array'), + expect(results).toEqual([ + { foo_id: 1, foo_value: 'hello' }, + { foo_id: 2, foo_value: 'world' }, + { foo_id: 3, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, ]); }); }); -test('batch endpoint (multiple requests, error handling, with reordering)', async () => { +test('batch endpoint with isResponseDictionary handles a response that returns a dictionary, with a missing item', async () => { const config = { resources: { foo: { isBatchResource: true, - docsLink: 'example.com/docs/bar', + docsLink: 'example.com/docs/foos', batchKey: 'foo_ids', newKey: 'foo_id', - reorderResultsByKey: 'foo_id', + isResponseDictionary: true, }, }, }; const resources = { - foo: ({ foo_ids, include_extra_info }) => { - if (_.isEqual(foo_ids, [1, 3])) { - expect(include_extra_info).toBe(false); - return new Error('yikes'); - } - - if (_.isEqual(foo_ids, [2, 4, 5])) { - expect(include_extra_info).toBe(true); - // return items deliberately out of order - return Promise.resolve([ - { - foo_id: 4, - foo_value: 'greetings', - extra_stuff: 'lorem ipsum', - }, - { - foo_id: 5, - foo_value: 'greetings', - extra_stuff: 'lorem ipsum', - }, - { - foo_id: 2, - foo_value: 'greetings', - extra_stuff: 'lorem ipsum', - }, - ]); - } + foo: ({ foo_ids }) => { + expect(foo_ids).toEqual([1, 2, 3]); + return Promise.resolve({ + 1: { foo_id: 1, foo_value: 'hello' }, + 3: { foo_id: 3, foo_value: '!' }, + }); }, }; await createDataLoaders(config, async (getLoaders) => { const loaders = getLoaders(resources); - const results = await loaders.foo.loadMany([ - { foo_id: 1, include_extra_info: false }, - { foo_id: 2, include_extra_info: true }, - { foo_id: 3, include_extra_info: false }, - { foo_id: 4, include_extra_info: true }, - { foo_id: 5, include_extra_info: true }, - ]); - - expect(results).toMatchObject([ - expect.toBeError(/yikes/), - { foo_id: 2, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, - expect.toBeError(/yikes/), - { foo_id: 4, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, - { foo_id: 5, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, + const results = await loaders.foo.loadMany([{ foo_id: 1 }, { foo_id: 2 }, { foo_id: 3 }]); + expect(results).toEqual([ + { foo_id: 1, foo_value: 'hello' }, + expect.toBeError( + '[dataloader-codegen :: foo] Could not find key = "2" in the response dict', + 'BatchItemNotFoundError', + ), + { foo_id: 3, foo_value: '!' }, ]); }); }); -/** - * Without reorderResultsByKey: - * If we requested 3 items, but the resource only returns 2, we don't know which - * response is missing. It's unsafe to return any results, so we must throw an - * error for the whole set of requests. - */ -test('batch endpoint without reorderResultsByKey throws error for response with non existant items', async () => { +test('middleware can transform the request args and the resource response', async () => { + function before(resourcePath, resourceArgs) { + expect(resourcePath).toEqual(['foo']); + expect(resourceArgs).toEqual([{ foo_ids: [100, 200, 300] }]); + + // modify the arguments to the resource + return [{ foo_ids: [1, 2, 3] }]; + } + + function after(resourcePath, response) { + expect(resourcePath).toEqual(['foo']); + expect(response).toEqual([ + { foo_id: 1, foo_value: 'hello' }, + { foo_id: 2, foo_value: 'world' }, + { foo_id: 3, foo_value: '!' }, + ]); + + return [ + { foo_id: 1, foo_value: 'goodbye' }, + { foo_id: 2, foo_value: 'world' }, + { foo_id: 3, foo_value: '?' }, + ]; + } + const config = { resources: { foo: { @@ -1045,61 +969,44 @@ test('batch endpoint without reorderResultsByKey throws error for response with }; const resources = { - foo: ({ foo_ids, bar }) => { - if (_.isEqual(foo_ids, [1, 2, 3])) { - return Promise.resolve([ - { - foo_id: 1, - foo_value: 'greetings', - extra_stuff: 'lorem ipsum', - }, - // deliberately omit 2 - { - foo_id: 3, - foo_value: 'greetings', - extra_stuff: 'lorem ipsum', - }, - ]); - } else if (_.isEqual(foo_ids, [4])) { - return Promise.resolve([ - { - foo_id: 4, - foo_value: 'greetings', - }, - ]); - } + foo: ({ foo_ids }) => { + expect(foo_ids).toEqual([1, 2, 3]); + return Promise.resolve([ + { foo_id: 1, foo_value: 'hello' }, + { foo_id: 2, foo_value: 'world' }, + { foo_id: 3, foo_value: '!' }, + ]); }, }; await createDataLoaders(config, async (getLoaders) => { - const loaders = getLoaders(resources); - - const results = await loaders.foo.loadMany([ - { foo_id: 1, include_extra_info: true }, - { foo_id: 2, include_extra_info: true }, - { foo_id: 3, include_extra_info: true }, - { foo_id: 4, include_extra_info: false }, - ]); + const loaders = getLoaders(resources, { resourceMiddleware: { before, after } }); - expect(results).toMatchObject([ - expect.toBeError( - '[dataloader-codegen :: foo] Resource returned 2 items, but we requested 3 items', - 'BatchItemNotFoundError', - ), - expect.toBeError( - '[dataloader-codegen :: foo] Resource returned 2 items, but we requested 3 items', - 'BatchItemNotFoundError', - ), - expect.toBeError( - '[dataloader-codegen :: foo] Resource returned 2 items, but we requested 3 items', - 'BatchItemNotFoundError', - ), - { foo_id: 4, foo_value: 'greetings' }, + const results = await loaders.foo.loadMany([{ foo_id: 100 }, { foo_id: 200 }, { foo_id: 300 }]); + expect(results).toEqual([ + { foo_id: 1, foo_value: 'goodbye' }, + { foo_id: 2, foo_value: 'world' }, + { foo_id: 3, foo_value: '?' }, ]); }); }); -test('batch endpoint with reorderResultsByKey handles response with non existant items', async () => { +test('[isBatchResource: true] returning custom errors from error handler is supported', async () => { + class MyCustomError extends Error { + constructor(...args) { + super(...args); + this.name = this.constructor.name; + this.foo = 'bar'; + Error.captureStackTrace(this, MyCustomError); + } + } + + function errorHandler(resourcePath, error) { + expect(resourcePath).toEqual(['foo']); + expect(error.message).toBe('yikes'); + return new MyCustomError('hello from custom error object'); + } + const config = { resources: { foo: { @@ -1107,31 +1014,32 @@ test('batch endpoint with reorderResultsByKey handles response with non existant docsLink: 'example.com/docs/bar', batchKey: 'foo_ids', newKey: 'foo_id', - reorderResultsByKey: 'foo_id', }, }, }; const resources = { - foo: ({ foo_ids, bar }) => { - if (_.isEqual(foo_ids, [1, 2, 3])) { + foo: ({ foo_ids, include_extra_info }) => { + if (_.isEqual(foo_ids, [1, 3])) { + expect(include_extra_info).toBe(false); + throw new Error('yikes'); + } + + if (_.isEqual(foo_ids, [2, 4, 5])) { + expect(include_extra_info).toBe(true); return Promise.resolve([ { - foo_id: 1, + foo_id: 2, foo_value: 'greetings', extra_stuff: 'lorem ipsum', }, - // deliberately omit 2 { - foo_id: 3, + foo_id: 4, foo_value: 'greetings', extra_stuff: 'lorem ipsum', }, - ]); - } else if (_.isEqual(foo_ids, [4])) { - return Promise.resolve([ { - foo_id: 4, + foo_id: 5, foo_value: 'greetings', extra_stuff: 'lorem ipsum', }, @@ -1141,213 +1049,97 @@ test('batch endpoint with reorderResultsByKey handles response with non existant }; await createDataLoaders(config, async (getLoaders) => { - const loaders = getLoaders(resources); + const loaders = getLoaders(resources, { errorHandler }); const results = await loaders.foo.loadMany([ - { foo_id: 1, bar: true }, - { foo_id: 2, bar: true }, - { foo_id: 3, bar: true }, - { foo_id: 4, bar: false }, + { foo_id: 1, include_extra_info: false }, + { foo_id: 2, include_extra_info: true }, + { foo_id: 3, include_extra_info: false }, + { foo_id: 4, include_extra_info: true }, + { foo_id: 5, include_extra_info: true }, ]); expect(results).toMatchObject([ - { foo_id: 1, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, - expect.toBeError( - '[dataloader-codegen :: foo] Response did not contain item with foo_id = 2', - 'BatchItemNotFoundError', - ), - { foo_id: 3, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, + expect.toBeError(/hello from custom error object/, 'MyCustomError'), + { foo_id: 2, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, + expect.toBeError(/hello from custom error object/, 'MyCustomError'), { foo_id: 4, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, + { foo_id: 5, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, ]); + + expect(results[0]).toHaveProperty('foo', 'bar'); + expect(results[2]).toHaveProperty('foo', 'bar'); }); }); -test('batch endpoint with isResponseDictionary handles a response that returns a dictionary', async () => { - const config = { - resources: { - foo: { - isBatchResource: true, - docsLink: 'example.com/docs/foos', - batchKey: 'foo_ids', - newKey: 'foo_id', - isResponseDictionary: true, - }, - }, - }; - - const resources = { - foo: ({ foo_ids }) => { - expect(foo_ids).toEqual([1, 2, 3]); - return Promise.resolve({ - 2: { foo_id: 2, foo_value: 'world' }, - 1: { foo_id: 1, foo_value: 'hello' }, - 3: { foo_id: 3, foo_value: '!' }, - }); - }, - }; - - await createDataLoaders(config, async (getLoaders) => { - const loaders = getLoaders(resources); +test('[isBatchResource: false] returning custom errors from error handler is supported', async () => { + class MyCustomError extends Error { + constructor(...args) { + super(...args); + this.name = this.constructor.name; + this.foo = 'bar'; + Error.captureStackTrace(this, MyCustomError); + } + } - const results = await loaders.foo.loadMany([{ foo_id: 1 }, { foo_id: 2 }, { foo_id: 3 }]); - expect(results).toEqual([ - { foo_id: 1, foo_value: 'hello' }, - { foo_id: 2, foo_value: 'world' }, - { foo_id: 3, foo_value: '!' }, - ]); - }); -}); + function errorHandler(resourcePath, error) { + expect(resourcePath).toEqual(['foo']); + expect(error.message).toBe('yikes'); + return new MyCustomError('hello from custom error object'); + } -test('batch endpoint with isBatchKeyASet handles a response', async () => { const config = { resources: { foo: { - isBatchResource: true, + isBatchResource: false, docsLink: 'example.com/docs/bar', - batchKey: 'foo_ids', - newKey: 'foo_id', - isBatchKeyASet: true, }, }, }; const resources = { - foo: ({ foo_ids, include_extra_info }) => { - if (_.isEqual(foo_ids, [1, 2])) { + foo: ({ foo_id, include_extra_info }) => { + if ([1, 3].includes(foo_id)) { expect(include_extra_info).toBe(false); - return Promise.resolve([ - { foo_id: 1, foo_value: 'hello' }, - { foo_id: 2, foo_value: 'world' }, - ]); + throw new Error('yikes'); } - if (_.isEqual(foo_ids, [3])) { + if ([2, 4, 5].includes(foo_id)) { expect(include_extra_info).toBe(true); - return Promise.resolve([ - { - foo_id: 3, - foo_value: 'greetings', - extra_stuff: 'lorem ipsum', - }, - ]); + return Promise.resolve({ + foo_id, + foo_value: 'greetings', + extra_stuff: 'lorem ipsum', + }); } }, }; await createDataLoaders(config, async (getLoaders) => { - const loaders = getLoaders(resources); + const loaders = getLoaders(resources, { errorHandler }); const results = await loaders.foo.loadMany([ { foo_id: 1, include_extra_info: false }, - { foo_id: 2, include_extra_info: false }, - { foo_id: 3, include_extra_info: true }, - ]); - - expect(results).toEqual([ - { foo_id: 1, foo_value: 'hello' }, - { foo_id: 2, foo_value: 'world' }, - { foo_id: 3, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, - ]); - }); -}); - -test('batch endpoint with isResponseDictionary handles a response that returns a dictionary, with a missing item', async () => { - const config = { - resources: { - foo: { - isBatchResource: true, - docsLink: 'example.com/docs/foos', - batchKey: 'foo_ids', - newKey: 'foo_id', - isResponseDictionary: true, - }, - }, - }; - - const resources = { - foo: ({ foo_ids }) => { - expect(foo_ids).toEqual([1, 2, 3]); - return Promise.resolve({ - 1: { foo_id: 1, foo_value: 'hello' }, - 3: { foo_id: 3, foo_value: '!' }, - }); - }, - }; - - await createDataLoaders(config, async (getLoaders) => { - const loaders = getLoaders(resources); - - const results = await loaders.foo.loadMany([{ foo_id: 1 }, { foo_id: 2 }, { foo_id: 3 }]); - expect(results).toEqual([ - { foo_id: 1, foo_value: 'hello' }, - expect.toBeError( - '[dataloader-codegen :: foo] Could not find key = "2" in the response dict', - 'BatchItemNotFoundError', - ), - { foo_id: 3, foo_value: '!' }, + { foo_id: 2, include_extra_info: true }, + { foo_id: 3, include_extra_info: false }, + { foo_id: 4, include_extra_info: true }, + { foo_id: 5, include_extra_info: true }, ]); - }); -}); - -test('middleware can transform the request args and the resource response', async () => { - function before(resourcePath, resourceArgs) { - expect(resourcePath).toEqual(['foo']); - expect(resourceArgs).toEqual([{ foo_ids: [100, 200, 300] }]); - - // modify the arguments to the resource - return [{ foo_ids: [1, 2, 3] }]; - } - function after(resourcePath, response) { - expect(resourcePath).toEqual(['foo']); - expect(response).toEqual([ - { foo_id: 1, foo_value: 'hello' }, - { foo_id: 2, foo_value: 'world' }, - { foo_id: 3, foo_value: '!' }, + expect(results).toMatchObject([ + expect.toBeError(/hello from custom error object/, 'MyCustomError'), + { foo_id: 2, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, + expect.toBeError(/hello from custom error object/, 'MyCustomError'), + { foo_id: 4, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, + { foo_id: 5, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, ]); - return [ - { foo_id: 1, foo_value: 'goodbye' }, - { foo_id: 2, foo_value: 'world' }, - { foo_id: 3, foo_value: '?' }, - ]; - } - - const config = { - resources: { - foo: { - isBatchResource: true, - docsLink: 'example.com/docs/bar', - batchKey: 'foo_ids', - newKey: 'foo_id', - }, - }, - }; - - const resources = { - foo: ({ foo_ids }) => { - expect(foo_ids).toEqual([1, 2, 3]); - return Promise.resolve([ - { foo_id: 1, foo_value: 'hello' }, - { foo_id: 2, foo_value: 'world' }, - { foo_id: 3, foo_value: '!' }, - ]); - }, - }; - - await createDataLoaders(config, async (getLoaders) => { - const loaders = getLoaders(resources, { resourceMiddleware: { before, after } }); - - const results = await loaders.foo.loadMany([{ foo_id: 100 }, { foo_id: 200 }, { foo_id: 300 }]); - expect(results).toEqual([ - { foo_id: 1, foo_value: 'goodbye' }, - { foo_id: 2, foo_value: 'world' }, - { foo_id: 3, foo_value: '?' }, - ]); + expect(results[0]).toHaveProperty('foo', 'bar'); + expect(results[2]).toHaveProperty('foo', 'bar'); }); }); -test('[isBatchResource: true] returning custom errors from error handler is supported', async () => { +test('bail if errorHandler does not return an error', async () => { class MyCustomError extends Error { constructor(...args) { super(...args); @@ -1360,7 +1152,7 @@ test('[isBatchResource: true] returning custom errors from error handler is supp function errorHandler(resourcePath, error) { expect(resourcePath).toEqual(['foo']); expect(error.message).toBe('yikes'); - return new MyCustomError('hello from custom error object'); + return 'not an Error object'; } const config = { @@ -1416,101 +1208,169 @@ test('[isBatchResource: true] returning custom errors from error handler is supp ]); expect(results).toMatchObject([ - expect.toBeError(/hello from custom error object/, 'MyCustomError'), + expect.toBeError(/errorHandler did not return an Error object. Instead, got string: 'not an Error object'/), { foo_id: 2, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, - expect.toBeError(/hello from custom error object/, 'MyCustomError'), + expect.toBeError(/errorHandler did not return an Error object. Instead, got string: 'not an Error object'/), { foo_id: 4, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, { foo_id: 5, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, ]); - - expect(results[0]).toHaveProperty('foo', 'bar'); - expect(results[2]).toHaveProperty('foo', 'bar'); }); }); -test('[isBatchResource: false] returning custom errors from error handler is supported', async () => { - class MyCustomError extends Error { - constructor(...args) { - super(...args); - this.name = this.constructor.name; - this.foo = 'bar'; - Error.captureStackTrace(this, MyCustomError); - } - } - - function errorHandler(resourcePath, error) { - expect(resourcePath).toEqual(['foo']); - expect(error.message).toBe('yikes'); - return new MyCustomError('hello from custom error object'); - } - +test('batch endpoint (multiple requests) with propertyBatchKey', async () => { const config = { resources: { foo: { - isBatchResource: false, + isBatchResource: true, docsLink: 'example.com/docs/bar', + batchKey: 'foo_ids', + newKey: 'foo_id', + propertyBatchKey: 'properties', + propertyNewKey: 'property', }, }, }; const resources = { - foo: ({ foo_id, include_extra_info }) => { - if ([1, 3].includes(foo_id)) { + foo: ({ foo_ids, properties, include_extra_info }) => { + if (_.isEqual(foo_ids, [2, 1])) { expect(include_extra_info).toBe(false); - throw new Error('yikes'); + return Promise.resolve([ + { foo_id: 1, rating: 3, name: 'Burger King' }, + { foo_id: 2, rating: 4, name: 'In N Out' }, + ]); } - if ([2, 4, 5].includes(foo_id)) { + if (_.isEqual(foo_ids, [3])) { expect(include_extra_info).toBe(true); - return Promise.resolve({ - foo_id, - foo_value: 'greetings', - extra_stuff: 'lorem ipsum', - }); + return Promise.resolve([ + { + foo_id: 3, + rating: 5, + name: 'Shake Shack', + extra_stuff: 'lorem ipsum', + }, + ]); } }, }; await createDataLoaders(config, async (getLoaders) => { - const loaders = getLoaders(resources, { errorHandler }); + const loaders = getLoaders(resources); const results = await loaders.foo.loadMany([ - { foo_id: 1, include_extra_info: false }, - { foo_id: 2, include_extra_info: true }, - { foo_id: 3, include_extra_info: false }, - { foo_id: 4, include_extra_info: true }, - { foo_id: 5, include_extra_info: true }, + { foo_id: 2, property: 'name', include_extra_info: false }, + { foo_id: 1, property: 'rating', include_extra_info: false }, + { foo_id: 3, property: 'rating', include_extra_info: true }, ]); - expect(results).toMatchObject([ - expect.toBeError(/hello from custom error object/, 'MyCustomError'), - { foo_id: 2, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, - expect.toBeError(/hello from custom error object/, 'MyCustomError'), - { foo_id: 4, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, - { foo_id: 5, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, + expect(results).toEqual([ + { foo_id: 2, name: 'In N Out' }, + { foo_id: 1, rating: 3 }, + { foo_id: 3, rating: 5 }, ]); + }); +}); - expect(results[0]).toHaveProperty('foo', 'bar'); - expect(results[2]).toHaveProperty('foo', 'bar'); +test('batch endpoint (multiple requests) with propertyBatchKey returned in a nested object ', async () => { + const config = { + resources: { + foo: { + isBatchResource: true, + docsLink: 'example.com/docs/bar', + batchKey: 'foo_ids', + newKey: 'foo_id', + propertyBatchKey: 'properties', + propertyNewKey: 'property', + }, + }, + }; + + const resources = { + foo: ({ foo_ids, properties, include_extra_info }) => { + if (_.isEqual(foo_ids, [2, 1])) { + expect(include_extra_info).toBe(false); + return Promise.resolve([ + { foo_id: 1, properties: { rating: 3, name: 'Burger King' } }, + { foo_id: 2, properties: { rating: 4, name: 'In N Out' } }, + ]); + } + + if (_.isEqual(foo_ids, [3])) { + expect(include_extra_info).toBe(true); + return Promise.resolve([ + { + foo_id: 3, + properties: { + rating: 5, + name: 'Shake Shack', + }, + }, + ]); + } + }, + }; + + await createDataLoaders(config, async (getLoaders) => { + const loaders = getLoaders(resources); + + const results = await loaders.foo.loadMany([ + { foo_id: 2, property: 'name', include_extra_info: false }, + { foo_id: 1, property: 'rating', include_extra_info: false }, + { foo_id: 3, property: 'rating', include_extra_info: true }, + ]); + + expect(results).toEqual([ + { foo_id: 2, properties: { name: 'In N Out' } }, + { foo_id: 1, properties: { rating: 3 } }, + { foo_id: 3, properties: { rating: 5 } }, + ]); }); }); -test('bail if errorHandler does not return an error', async () => { - class MyCustomError extends Error { - constructor(...args) { - super(...args); - this.name = this.constructor.name; - this.foo = 'bar'; - Error.captureStackTrace(this, MyCustomError); - } - } +test('batch endpoint (with commaSeparatedBatchKey) with propertyBatchKey', async () => { + const config = { + resources: { + foo: { + isBatchResource: true, + docsLink: 'example.com/docs/bar', + batchKey: 'foo_ids', + newKey: 'foo_id', + commaSeparatedBatchKey: true, + propertyBatchKey: 'properties', + propertyNewKey: 'property', + }, + }, + }; - function errorHandler(resourcePath, error) { - expect(resourcePath).toEqual(['foo']); - expect(error.message).toBe('yikes'); - return 'not an Error object'; - } + const resources = { + foo: ({ foo_ids }) => { + expect(foo_ids).toEqual('2,1,3'); + return Promise.resolve([ + { foo_id: 1, properties: { rating: 3, name: 'Burger King' } }, + { foo_id: 2, properties: { rating: 4, name: 'Shake Shack' } }, + { foo_id: 3, properties: { rating: 5, name: 'In N Out' } }, + ]); + }, + }; + await createDataLoaders(config, async (getLoaders) => { + const loaders = getLoaders(resources); + + const results = await loaders.foo.loadMany([ + { foo_id: 2, property: 'name' }, + { foo_id: 1, property: 'rating' }, + { foo_id: 3, property: 'rating' }, + ]); + expect(results).toEqual([ + { foo_id: 2, properties: { name: 'Shake Shack' } }, + { foo_id: 1, properties: { rating: 3 } }, + { foo_id: 3, properties: { rating: 5 } }, + ]); + }); +}); + +test('batch endpoint (with nestedPath) with propertyBatchKey', async () => { const config = { resources: { foo: { @@ -1518,57 +1378,327 @@ test('bail if errorHandler does not return an error', async () => { docsLink: 'example.com/docs/bar', batchKey: 'foo_ids', newKey: 'foo_id', + nestedPath: 'foos', + propertyBatchKey: 'properties', + propertyNewKey: 'property', }, }, }; const resources = { - foo: ({ foo_ids, include_extra_info }) => { + foo: ({ foo_ids }) => { + expect(foo_ids).toEqual([2, 1, 3]); + return Promise.resolve({ + foos: [ + { foo_id: 1, properties: { rating: 3, name: 'Burger King' } }, + { foo_id: 2, properties: { rating: 4, name: 'Shake Shack' } }, + { foo_id: 3, properties: { rating: 5, name: 'In N Out' } }, + ], + }); + }, + }; + + await createDataLoaders(config, async (getLoaders) => { + const loaders = getLoaders(resources); + + const results = await loaders.foo.loadMany([ + { foo_id: 2, property: 'name' }, + { foo_id: 1, property: 'rating' }, + { foo_id: 3, property: 'rating' }, + ]); + expect(results).toEqual([ + { foo_id: 2, properties: { name: 'Shake Shack' } }, + { foo_id: 1, properties: { rating: 3 } }, + { foo_id: 3, properties: { rating: 5 } }, + ]); + }); +}); + +test('batch endpoint (multiple requests) with propertyBatchKey that rejects', async () => { + const config = { + resources: { + foo: { + isBatchResource: true, + docsLink: 'example.com/docs/bar', + batchKey: 'foo_ids', + newKey: 'foo_id', + propertyBatchKey: 'properties', + propertyNewKey: 'property', + }, + }, + }; + + const resources = { + foo: ({ foo_ids, properties, include_extra_info }) => { if (_.isEqual(foo_ids, [1, 3])) { expect(include_extra_info).toBe(false); - throw new Error('yikes'); + return Promise.reject('yikes'); } - if (_.isEqual(foo_ids, [2, 4, 5])) { expect(include_extra_info).toBe(true); return Promise.resolve([ { foo_id: 2, - foo_value: 'greetings', - extra_stuff: 'lorem ipsum', + name: 'Burger King', + rating: 3, }, { foo_id: 4, - foo_value: 'greetings', + name: 'In N Out', + rating: 3.5, + }, + { + foo_id: 5, + name: 'Shake Shack', + rating: 4, extra_stuff: 'lorem ipsum', }, + ]); + } + }, + }; + + await createDataLoaders(config, async (getLoaders) => { + const loaders = getLoaders(resources); + + const results = await loaders.foo.loadMany([ + { foo_id: 1, property: 'name', include_extra_info: false }, + { foo_id: 2, property: 'rating', include_extra_info: true }, + { foo_id: 3, property: 'name', include_extra_info: false }, + { foo_id: 4, property: 'rating', include_extra_info: true }, + { foo_id: 5, property: 'name', include_extra_info: true }, + ]); + + expect(results).toMatchObject([ + expect.toBeError(/yikes/, 'NonError'), + { foo_id: 2, rating: 3 }, + expect.toBeError(/yikes/, 'NonError'), + { foo_id: 4, rating: 3.5 }, + { foo_id: 5, name: 'Shake Shack' }, + ]); + }); +}); + +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', + propertyNewKey: 'property', + }, + }, + }; + + 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([ + { + foo_id: 2, + name: 'Burger King', + rating: 3, + }, + { + foo_id: 4, + name: 'In N Out', + rating: 3.5, + }, { foo_id: 5, - foo_value: 'greetings', + name: 'Shake Shack', + rating: 4, extra_stuff: 'lorem ipsum', }, ]); } + 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, { errorHandler }); + const loaders = getLoaders(resources); const results = await loaders.foo.loadMany([ - { foo_id: 1, include_extra_info: false }, - { foo_id: 2, include_extra_info: true }, - { foo_id: 3, include_extra_info: false }, - { foo_id: 4, include_extra_info: true }, - { foo_id: 5, include_extra_info: true }, + { foo_id: 1, property: 'name', include_extra_info: false }, + { foo_id: 2, property: 'rating', include_extra_info: true }, + { foo_id: 3, property: 'name', include_extra_info: false }, + { foo_id: 4, property: 'rating', include_extra_info: true }, + { foo_id: 5, property: 'name', include_extra_info: true }, ]); expect(results).toMatchObject([ - expect.toBeError(/errorHandler did not return an Error object. Instead, got string: 'not an Error object'/), - { foo_id: 2, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, - expect.toBeError(/errorHandler did not return an Error object. Instead, got string: 'not an Error object'/), - { foo_id: 4, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, - { foo_id: 5, foo_value: 'greetings', extra_stuff: 'lorem ipsum' }, + expect.toBeError(/yikes/), + { foo_id: 2, rating: 3 }, + expect.toBeError(/yikes/), + { foo_id: 4, rating: 3.5 }, + { foo_id: 5, name: 'Shake Shack' }, ]); }); }); + +test('batch endpoint with propertyBatchKey without reorderResultsByKey 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', + propertyNewKey: 'property', + }, + }, + }; + + const resources = { + foo: ({ foo_ids, bar }) => { + if (_.isEqual(foo_ids, [1, 2, 3])) { + 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])) { + 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, property: 'name', include_extra_info: true }, + { foo_id: 2, property: 'rating', include_extra_info: true }, + { foo_id: 3, property: 'rating', include_extra_info: true }, + { foo_id: 4, property: 'rating', include_extra_info: false }, + ]); + + expect(results).toMatchObject([ + { foo_id: 1, name: 'Shake Shack' }, + expect.toBeError( + 'Could not find key = "2" in the response dict. Or your response does not follow the type we support.', + 'BatchItemNotFoundError', + ), + { foo_id: 3, rating: 3 }, + { foo_id: 4, rating: 3.5 }, + ]); + }); +}); + +test('batch endpoint with propertyBatchKey with isResponseDictionary with a missing item', async () => { + const config = { + resources: { + foo: { + isBatchResource: true, + docsLink: 'example.com/docs/foos', + batchKey: 'foo_ids', + newKey: 'foo_id', + isResponseDictionary: true, + propertyBatchKey: 'properties', + propertyNewKey: 'property', + }, + }, + }; + + const resources = { + foo: ({ foo_ids }) => { + expect(foo_ids).toEqual([2, 1, 3]); + return Promise.resolve({ + 1: { foo_id: 1, properties: { rating: 3, name: 'Burger King' } }, + 3: { foo_id: 3, properties: { rating: 4, name: 'Shake Shack' } }, + }); + }, + }; + console.log('aaaa'); + await createDataLoaders(config, async (getLoaders) => { + const loaders = getLoaders(resources); + + const results = await loaders.foo.loadMany([ + { foo_id: 2, property: 'name' }, + { foo_id: 1, property: 'rating' }, + { foo_id: 3, property: 'rating' }, + ]); + expect(results).toEqual([ + expect.toBeError( + 'Could not find key = "2" in the response dict. Or your response does not follow the type we support.', + 'BatchItemNotFoundError', + ), + { foo_id: 1, properties: { rating: 3 } }, + { foo_id: 3, properties: { rating: 4 } }, + ]); + }); +}); + +test('batch endpoint (multiple requests) with propertyBatchKey different response structure', async () => { + const config = { + resources: { + foo: { + isBatchResource: true, + docsLink: 'example.com/docs/bar', + batchKey: 'foo_ids', + newKey: 'foo_id', + propertyBatchKey: 'properties', + propertyNewKey: 'property', + }, + }, + }; + + const resources = { + foo: ({ foo_ids, properties, include_extra_info }) => { + if (_.isEqual(foo_ids, [2, 1]) && _.isEqual(properties, ['name', 'rating'])) { + expect(include_extra_info).toBe(false); + return Promise.resolve([ + { 1: { rating: 3, name: 'Burger King' } }, + { 2: { rating: 4, name: 'In N Out' } }, + ]); + } + + if (_.isEqual(foo_ids, [3]) && _.isEqual(properties, ['rating'])) { + expect(include_extra_info).toBe(true); + return Promise.resolve([ + { + 3: { + rating: 5, + extra_stuff: 'lorem ipsum', + }, + }, + ]); + } + }, + }; + + await createDataLoaders(config, async (getLoaders) => { + const loaders = getLoaders(resources); + + const results = await loaders.foo.loadMany([ + { foo_id: 2, property: 'name', include_extra_info: false }, + { foo_id: 1, property: 'rating', include_extra_info: false }, + { foo_id: 3, property: 'rating', include_extra_info: true }, + ]); + + expect(results).toEqual([{ 2: { name: 'In N Out' } }, { 1: { rating: 3 } }, { 3: { rating: 5 } }]); + }); +}); diff --git a/src/runtimeHelpers.ts b/src/runtimeHelpers.ts index b76a3f8..e1cc0ce 100644 --- a/src/runtimeHelpers.ts +++ b/src/runtimeHelpers.ts @@ -396,6 +396,7 @@ export function unPartitionResultsByBatchKeyPartition( } } if (result === null) { + console.log('ffff'); return { order: id, result: new BatchItemNotFoundError( From 8366711924b1c00cd01dc2ba1403c5a12b1aa6da Mon Sep 17 00:00:00 2001 From: Yue Guo Date: Fri, 21 May 2021 15:56:05 -0700 Subject: [PATCH 15/24] fix some review feedback and add more tests --- Makefile | 2 +- __tests__/implementation.test.js | 160 ++++++++++++++++++++++++++++++- schema.json | 4 +- src/implementation.ts | 36 ++++--- src/runtimeHelpers.ts | 12 ++- 5 files changed, 191 insertions(+), 23 deletions(-) diff --git a/Makefile b/Makefile index d2c5104..f86f502 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ minimal: node_modules build venv: Makefile requirements-dev.txt rm -rf venv - $(PYTHON3) -m venv venv + virtualenv venv --python=$(PYTHON3) venv/bin/pip install -r requirements-dev.txt node_modules: package.json yarn.lock diff --git a/__tests__/implementation.test.js b/__tests__/implementation.test.js index ee1d013..0563188 100644 --- a/__tests__/implementation.test.js +++ b/__tests__/implementation.test.js @@ -1632,7 +1632,7 @@ test('batch endpoint with propertyBatchKey with isResponseDictionary with a miss }); }, }; - console.log('aaaa'); + await createDataLoaders(config, async (getLoaders) => { const loaders = getLoaders(resources); @@ -1702,3 +1702,161 @@ test('batch endpoint (multiple requests) with propertyBatchKey different respons expect(results).toEqual([{ 2: { name: 'In N Out' } }, { 1: { rating: 3 } }, { 3: { rating: 5 } }]); }); }); + +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', + propertyNewKey: 'property', + }, + }, + }; + + const resources = { + foo: ({ foo_ids, bar }) => { + if (_.isEqual(foo_ids, [1, 2, 3])) { + return Promise.resolve([ + { foo_id: 3, properties: { rating: 4, name: 'Shake Shack' } }, + { foo_id: 1, properties: { rating: 3, name: 'Burger King' } }, + // deliberately omit 2 + ]); + } else if (_.isEqual(foo_ids, [4])) { + return Promise.resolve([{ foo_id: 4, properties: { rating: 5, name: 'In N Out' } }]); + } + }, + }; + await createDataLoaders(config, async (getLoaders) => { + const loaders = getLoaders(resources); + + const results = await loaders.foo.loadMany([ + { foo_id: 1, property: 'rating', bar: true }, + { foo_id: 2, property: 'name', bar: true }, + { foo_id: 3, property: 'rating', bar: true }, + { foo_id: 4, property: 'rating', bar: false }, + ]); + + expect(results).toMatchObject([ + { foo_id: 1, properties: { rating: 3 } }, + expect.toBeError( + 'Could not find key = "2" in the response dict. Or your response does not follow the type we support.', + 'BatchItemNotFoundError', + ), + { foo_id: 3, properties: { rating: 4 } }, + { foo_id: 4, properties: { rating: 5 } }, + ]); + }); +}); + +test('batch endpoint with propertyBatchKey (multiple requests, error handling - non array response)', async () => { + const config = { + eek: true, + resources: { + foo: { + isBatchResource: true, + docsLink: 'example.com/docs/bar', + batchKey: 'foo_ids', + newKey: 'foo_id', + propertyBatchKey: 'properties', + propertyNewKey: 'property', + }, + }, + }; + + const resources = { + foo: ({ foo_ids, include_extra_info }) => { + if (_.isEqual(foo_ids, [1, 3])) { + expect(include_extra_info).toBe(false); + return new Error('yikes'); + } + + if (_.isEqual(foo_ids, [2, 4, 5])) { + expect(include_extra_info).toBe(true); + // Deliberately returning an object, not an array + return Promise.resolve({ + foo: 'bar', + }); + } + }, + }; + + await createDataLoaders(config, async (getLoaders) => { + const loaders = getLoaders(resources); + + const results = await loaders.foo.loadMany([ + { foo_id: 1, property: 'name', include_extra_info: false }, + { foo_id: 2, property: 'rating', include_extra_info: true }, + { foo_id: 3, property: 'rating', include_extra_info: false }, + { foo_id: 4, property: 'name', include_extra_info: true }, + { foo_id: 5, property: 'name', include_extra_info: true }, + ]); + + expect(results).toMatchObject([ + expect.toBeError('yikes'), + expect.toBeError('[dataloader-codegen :: foo] Expected response to be an array'), + expect.toBeError('yikes'), + expect.toBeError('[dataloader-codegen :: foo] Expected response to be an array'), + expect.toBeError('[dataloader-codegen :: foo] Expected response to be an array'), + ]); + }); +}); + +test('batch endpoint with propertyBatchKey (multiple requests, error handling, with reordering)', async () => { + const config = { + resources: { + foo: { + isBatchResource: true, + docsLink: 'example.com/docs/bar', + batchKey: 'foo_ids', + newKey: 'foo_id', + reorderResultsByKey: 'foo_id', + propertyBatchKey: 'properties', + propertyNewKey: 'property', + }, + }, + }; + + const resources = { + foo: ({ foo_ids, include_extra_info }) => { + if (_.isEqual(foo_ids, [1, 3])) { + expect(include_extra_info).toBe(false); + return new Error('yikes'); + } + + if (_.isEqual(foo_ids, [2, 4, 5])) { + expect(include_extra_info).toBe(true); + // return items deliberately out of order + return Promise.resolve([ + { foo_id: 4, properties: { rating: 4, name: 'Shake Shack' } }, + { foo_id: 5, properties: { rating: 3, name: 'Burger King' } }, + { foo_id: 2, properties: { rating: 5, name: 'In N Out' } }, + ]); + } + }, + }; + + await createDataLoaders(config, async (getLoaders) => { + const loaders = getLoaders(resources); + + const results = await loaders.foo.loadMany([ + { foo_id: 1, property: 'name', include_extra_info: false }, + { foo_id: 2, property: 'rating', include_extra_info: true }, + { foo_id: 3, property: 'rating', include_extra_info: false }, + { foo_id: 4, property: 'name', include_extra_info: true }, + { foo_id: 5, property: 'name', include_extra_info: true }, + ]); + + expect(results).toEqual([ + expect.toBeError(/yikes/), + { foo_id: 2, properties: { rating: 5 } }, + expect.toBeError(/yikes/), + { foo_id: 4, properties: { name: 'Shake Shack' } }, + { foo_id: 5, properties: { name: 'Burger King' } }, + ]); + }); +}); diff --git a/schema.json b/schema.json index 5337007..99f0f10 100644 --- a/schema.json +++ b/schema.json @@ -100,11 +100,11 @@ }, "propertyBatchKey": { "type": "string", - "description": "The argument to the resource that represents the list of properties we want to fetch. (e.g. 'properties', 'features')" + "description": "(Optional) The argument to the resource that represents the nested list of optional properties we want to fetch. (e.g. usually 'properties' or 'features')" }, "propertyNewKey": { "type": "string", - "description": "The argument we'll replace the propertyBatchKey with - should be a singular version of the batchKey (e.g. 'property', 'feature')" + "description": "(Optional) The argument we'll replace the propertyBatchKey with - should be a singular version of the propertyBatchKey (e.g. usually 'property' or 'feature')" } } }, diff --git a/src/implementation.ts b/src/implementation.ts index 1acf9f5..2b77a47 100644 --- a/src/implementation.ts +++ b/src/implementation.ts @@ -423,19 +423,28 @@ function getBatchLoader(resourceConfig: BatchResourceConfig, resourcePath: Reado return response; })) - if (${typeof resourceConfig.propertyNewKey === 'string'} && ${ - typeof resourceConfig.propertyBatchKey === 'string' - }) { - const batchKeyPartition = getBatchKeysForPartitionItems('${resourceConfig.newKey}', ['${ - resourceConfig.newKey - }', '${resourceConfig.propertyNewKey}'], keys); - const propertyBatchKeyPartiion = getBatchKeysForPartitionItems('${resourceConfig.propertyNewKey}', ['${ - resourceConfig.newKey - }', '${resourceConfig.propertyNewKey}'], keys); - // Split the results back up into the order that they were requested - return unPartitionResultsByBatchKeyPartition('${resourceConfig.newKey}', '${ - resourceConfig.propertyBatchKey - }', batchKeyPartition, propertyBatchKeyPartiion, requestGroups, groupedResults); + if ( + ${typeof resourceConfig.propertyNewKey === 'string'} && + ${typeof resourceConfig.propertyBatchKey === 'string'}) { + const batchKeyPartition = getBatchKeysForPartitionItems( + '${resourceConfig.newKey}', + ['${resourceConfig.newKey}', '${resourceConfig.propertyNewKey}'], + keys + ); + const propertyBatchKeyPartiion = getBatchKeysForPartitionItems( + '${resourceConfig.propertyNewKey}', + ['${resourceConfig.newKey}', '${resourceConfig.propertyNewKey}'], + keys + ); + // Split the results back up into the order that they were requested when there is propertyBatchKey + return unPartitionResultsByBatchKeyPartition( + '${resourceConfig.newKey}', + '${resourceConfig.propertyBatchKey}', + batchKeyPartition, + propertyBatchKeyPartiion, + requestGroups, + groupedResults + ); } else { // Split the results back up into the order that they were requested return unPartitionResults(requestGroups, groupedResults); @@ -493,6 +502,5 @@ export default function getLoaderImplementation(resourceConfig: ResourceConfig, const loader = resourceConfig.isBatchResource ? getBatchLoader(resourceConfig, resourcePath) : getNonBatchLoader(resourceConfig, resourcePath); - return loader; } diff --git a/src/runtimeHelpers.ts b/src/runtimeHelpers.ts index e1cc0ce..2753dd1 100644 --- a/src/runtimeHelpers.ts +++ b/src/runtimeHelpers.ts @@ -348,7 +348,6 @@ export function unPartitionResultsByBatchKeyPartition( * ] * ``` */ - const zippedGroups: ReadonlyArray> = requestGroups.map( (ids, i) => { return ids.map((id, j) => { @@ -363,7 +362,10 @@ export function unPartitionResultsByBatchKeyPartition( if (Object.values(element).includes(batchKeyPartition[i][j])) { // case 2, propertyBatchKey is returned in a nested object // { bar_id: 2, properties: {name: 'Burger King', rating: 3 }} - if (propertyBatchKey in element && propertyBatchKeyPartion[i][j] in element[propertyBatchKey]) { + if ( + element.hasOwnProperty(propertyBatchKey) && + element[propertyBatchKey].hasOwnProperty(propertyBatchKeyPartion[i][j]) + ) { result = element; result[propertyBatchKey] = { [propertyBatchKeyPartion[i][j]]: @@ -383,8 +385,8 @@ export function unPartitionResultsByBatchKeyPartition( // case 4, the value of newKey is the key and its properties are its values. // { 2: { name: 'Burger King', rating: 3 }} if ( - batchKeyPartition[i][j] in element && - propertyBatchKeyPartion[i][j] in element[batchKeyPartition[i][j]] + element.hasOwnProperty(batchKeyPartition[i][j]) && + element[batchKeyPartition[i][j]].hasOwnProperty(propertyBatchKeyPartion[i][j]) ) { result = { [batchKeyPartition[i][j]]: { @@ -395,8 +397,8 @@ export function unPartitionResultsByBatchKeyPartition( break; } } + if (result === null) { - console.log('ffff'); return { order: id, result: new BatchItemNotFoundError( From 15f56702d5f3beb152fdcbfcbd9054603c6d8cbe Mon Sep 17 00:00:00 2001 From: Yue Guo Date: Tue, 1 Jun 2021 12:17:25 -0700 Subject: [PATCH 16/24] add more comments --- __tests__/implementation.test.js | 6 +- src/implementation.ts | 57 +++++++++++++++- src/runtimeHelpers.ts | 113 ++++++++++++++++++++++--------- 3 files changed, 141 insertions(+), 35 deletions(-) diff --git a/__tests__/implementation.test.js b/__tests__/implementation.test.js index 0563188..fe6f198 100644 --- a/__tests__/implementation.test.js +++ b/__tests__/implementation.test.js @@ -1599,7 +1599,7 @@ test('batch endpoint with propertyBatchKey without reorderResultsByKey throws er expect(results).toMatchObject([ { foo_id: 1, name: 'Shake Shack' }, expect.toBeError( - 'Could not find key = "2" in the response dict. Or your response does not follow the type we support.', + 'Could not find newKey = "2" and propertyNewKey = "rating" in the response dict. Or your endpoint does not follow the contract we support.', 'BatchItemNotFoundError', ), { foo_id: 3, rating: 3 }, @@ -1643,7 +1643,7 @@ test('batch endpoint with propertyBatchKey with isResponseDictionary with a miss ]); expect(results).toEqual([ expect.toBeError( - 'Could not find key = "2" in the response dict. Or your response does not follow the type we support.', + 'Could not find newKey = "2" and propertyNewKey = "name" in the response dict. Or your endpoint does not follow the contract we support.', 'BatchItemNotFoundError', ), { foo_id: 1, properties: { rating: 3 } }, @@ -1744,7 +1744,7 @@ test('batch endpoint with propertyBatchKey with reorderResultsByKey handles resp expect(results).toMatchObject([ { foo_id: 1, properties: { rating: 3 } }, expect.toBeError( - 'Could not find key = "2" in the response dict. Or your response does not follow the type we support.', + 'Could not find newKey = "2" and propertyNewKey = "name" in the response dict. Or your endpoint does not follow the contract we support.', 'BatchItemNotFoundError', ), { foo_id: 3, properties: { rating: 4 } }, diff --git a/src/implementation.ts b/src/implementation.ts index 2b77a47..fc19980 100644 --- a/src/implementation.ts +++ b/src/implementation.ts @@ -163,6 +163,20 @@ function getBatchLoader(resourceConfig: BatchResourceConfig, resourcePath: Reado * Returns: * \`[ [ 0, 2 ], [ 1 ] ]\` * + * We could also have more than one batch key. + * + * Example: + * + * \`\`\`js + * partitionItems([ + * { [bar_id: 7, property: 'property_1'], include_extra_info: true }, + * { [bar_id: 8, property: 'property_2'], include_extra_info: false }, + * { [bar_id: 9, property: 'property_3'], include_extra_info: true }, + * ], 'bar_id') + * \`\`\` + * + * Returns: + * \`[ [ 0, 2 ], [ 1 ] ]\` * We'll refer to each element in the group as a "request ID". */ let requestGroups; @@ -310,6 +324,11 @@ function getBatchLoader(resourceConfig: BatchResourceConfig, resourcePath: Reado if ( !isResponseDictionary && reorderResultsByKey == null && + /** + * When there's propertyBatchKey and propertyNewKey, the resource might + * contain less number of items that we requested. It's valid, so we + * should skip the check. + */ !(typeof propertyNewKey === 'string' && typeof propertyBatchKey === 'string') ) { return ` @@ -331,6 +350,7 @@ function getBatchLoader(resourceConfig: BatchResourceConfig, resourcePath: Reado )} Resource returned \${response.length} items, but we requested \${requests.length} items.\`, 'Add reorderResultsByKey to the config for this resource to be able to handle a partial response.', ].join(' ')); + // Tell flow that BatchItemNotFoundError extends Error. // It's an issue with flowgen package, but not an issue with Flow. // @see https://github.com/Yelp/dataloader-codegen/pull/35#discussion_r394777533 @@ -423,6 +443,41 @@ function getBatchLoader(resourceConfig: BatchResourceConfig, resourcePath: Reado return response; })) + /** + * When there's propertyBatchKey and propertyNewKey, the resource might + * contain less number of items that we requested. We need the value of batchKey and + * propertyBatchKey in requests group to help us split the results back up into the + * order that they were requested. + * + * + * Example: + * + * getBatchKeyForPartitionItems( + * 'bar_id', + * ['bar_id', 'property'], + * [ + * { bar_id: 2, property: 'name', include_extra_info: true }, + * { bar_id: 3, property: 'rating', include_extra_info: false }, + * { bar_id: 4, property: 'rating', include_extra_info: true }, + * ]) + * + * + * Returns: + * [ [ 2, 4 ], [ 3 ] ] + * + * getBatchKeyForPartitionItems( + * 'property', + * ['bar_id', 'property'], + * [ + * { bar_id: 2, property: 'name', include_extra_info: true }, + * { bar_id: 3, property: 'rating', include_extra_info: false }, + * { bar_id: 4, property: 'rating', include_extra_info: true }, + * ]) + * + * + * Returns: + * [ [ 'name', 'rating' ], [ 'rating' ] ] + */ if ( ${typeof resourceConfig.propertyNewKey === 'string'} && ${typeof resourceConfig.propertyBatchKey === 'string'}) { @@ -436,7 +491,7 @@ function getBatchLoader(resourceConfig: BatchResourceConfig, resourcePath: Reado ['${resourceConfig.newKey}', '${resourceConfig.propertyNewKey}'], keys ); - // Split the results back up into the order that they were requested when there is propertyBatchKey + return unPartitionResultsByBatchKeyPartition( '${resourceConfig.newKey}', '${resourceConfig.propertyBatchKey}', diff --git a/src/runtimeHelpers.ts b/src/runtimeHelpers.ts index 2753dd1..3cec2bd 100644 --- a/src/runtimeHelpers.ts +++ b/src/runtimeHelpers.ts @@ -299,19 +299,54 @@ export function unPartitionResults( /** * Perform the inverse mapping from partitionItems on the nested results we get - * back from the service. This function is only called when we have propertyBatchKey. - * We assume the newKey is inside the response here. + * back from the service. This function is only called when we have propertyBatchKey and propertyNewKey. + * We only three different kind of response contract. + * + * Case 1: propertyBatchKey is returned in a nested object, newKey is at the top level. + * If we have 'bar_id' as newKey and 'properties' as propertyBatchKey, + * the resultGroups should look like this: + * [ + * [ { bar_id: 2, properties: { name: 'Burger King', rating: 3 } } ], + * [ { bar_id: 1, properties: { name: 'In N Out', rating: 4 } } ] + * ], + * + * Case 2: propertyBatchKey is not returned in a nested object, but spread at top level as well. + * If we have 'bar_id' as newKey and 'properties' as propertyBatchKey, + * the resultGroups should look like this: + * [ + * [ { bar_id: 2, name: 'Burger King', rating: 3 } ], + * [ { bar_id: 1, name: 'In N Out', rating: 4 } ] + * ], + * + * Case 3: newKey and its properties are returned as a key-value pair at the top level. + * If we have 'bar_id' as newKey and 'properties' as propertyBatchKey, + * the resultGroups should look like this: + * [ + * [ { 2: { name: 'Burger King', rating: 3 } } ], + * [ { 1: { name: 'In N Out', rating: 4 } } ] + * ], + * + * IMPORTANT NOTE: The contract must have a one-to-one correspondence between the input propertyBatchKey and the output propertyBatchKey. + * i.e. if we have property: 'name' in the request, the response must have 'name' in it, and no extra data assciated with it. * * Example + * Request args: + * [ + * { bar_id: 2, property: 'name', include_extra_info: true }, + * { bar_id: 1, property: 'rating', include_extra_info: false }, + * { bar_id: 2, property: 'rating', include_extra_info: true }, + * ] + * * ```js * unPartitionResultsByBatchKeyPartition( - * 'bar_id', - * [ ['name', 'rating'], ['rating'] ], - * [ [2, 2], [1] ], - * [ [0, 2], [1] ], - * [ + * newKey = 'bar_id', + * propertyBatchKey = 'properties', + * batchKeyPartition = [ [2, 2], [1] ], + * propertyBatchKeyPartion = [ ['name', 'rating'], ['rating'] ], + * requestGroups = [ [0, 2], [1] ], + * resultGroups = [ * [ { bar_id: 2, name: 'Burger King', rating: 3 } ], - * [ { bar_id: 1, name: 'In N Out', rating: 4 } ] + * [ { bar_id: 1, name: 'In N Out', rating: 4 } ] * ], * ) * ``` @@ -352,57 +387,73 @@ export function unPartitionResultsByBatchKeyPartition( (ids, i) => { return ids.map((id, j) => { let result = null; - for (const element of Object.values(resultGroups)[i]) { - // case 1, error in the result. - if (element instanceof CaughtResourceError) { - result = element; + for (const resultElement of Object.values(resultGroups)[i]) { + // There's error in the result we should return + if (resultElement instanceof CaughtResourceError) { + result = resultElement; break; } - // newKey has its own key-value pair at the response top level. - if (Object.values(element).includes(batchKeyPartition[i][j])) { - // case 2, propertyBatchKey is returned in a nested object - // { bar_id: 2, properties: {name: 'Burger King', rating: 3 }} + // newKey is returned at the response top level. + if (Object.values(resultElement).includes(batchKeyPartition[i][j])) { + /** + * Case 1: propertyBatchKey is returned in a nested object, newKey is at the top level. + * e.g.: { bar_id: 2, properties: { name: 'Burger King', rating: 3 } } + * If requested property exist in resultElement, we restructure data, so that we only return + * the property we asked for. + */ if ( - element.hasOwnProperty(propertyBatchKey) && - element[propertyBatchKey].hasOwnProperty(propertyBatchKeyPartion[i][j]) + resultElement.hasOwnProperty(propertyBatchKey) && + resultElement[propertyBatchKey].hasOwnProperty(propertyBatchKeyPartion[i][j]) ) { - result = element; + result = resultElement; result[propertyBatchKey] = { [propertyBatchKeyPartion[i][j]]: - element[propertyBatchKey][propertyBatchKeyPartion[i][j]], + resultElement[propertyBatchKey][propertyBatchKeyPartion[i][j]], }; - } - // case 3, propertyBatchKey is not returned in a nested object, but also at the top level. - // { bar_id: 2, name: 'Burger King', rating: 3 } - else { + } else { + /** + * Case 2: propertyBatchKey is not returned in a nested object, but spread at top level as well. + * e.g.: { bar_id: 2, name: 'Burger King', rating: 3 } + * We restructure data, so that we only leave newKey and the property we asked for at the top level. + */ result = Object.assign( {}, - ...[newKey, propertyBatchKeyPartion[i][j]].map((key) => ({ [key]: element[key] })), + ...[newKey, propertyBatchKeyPartion[i][j]].map((key) => ({ + [key]: resultElement[key], + })), ); } break; } - // case 4, the value of newKey is the key and its properties are its values. - // { 2: { name: 'Burger King', rating: 3 }} + /** + * Case 3: newKey and its properties are returned as a key-value pair at the top level. + * e.g.: { 2: { name: 'Burger King', rating: 3 } } + * If requested property exist in resultElement, we restructure data, so that we only return + * the property we asked for. + */ if ( - element.hasOwnProperty(batchKeyPartition[i][j]) && - element[batchKeyPartition[i][j]].hasOwnProperty(propertyBatchKeyPartion[i][j]) + resultElement.hasOwnProperty(batchKeyPartition[i][j]) && + resultElement[batchKeyPartition[i][j]].hasOwnProperty(propertyBatchKeyPartion[i][j]) ) { result = { [batchKeyPartition[i][j]]: { [propertyBatchKeyPartion[i][j]]: - element[batchKeyPartition[i][j]][propertyBatchKeyPartion[i][j]], + resultElement[batchKeyPartition[i][j]][propertyBatchKeyPartion[i][j]], }, }; break; } } + // If requested property doesn't exist in resultElement, we should throw BatchItemNotFoundError error. if (result === null) { return { order: id, result: new BatchItemNotFoundError( - `Could not find key = "${batchKeyPartition[i][j]}" in the response dict. Or your response does not follow the type we support.`, + [ + `Could not find newKey = "${batchKeyPartition[i][j]}" and propertyNewKey = "${propertyBatchKeyPartion[i][j]}" in the response dict.`, + `Or your endpoint does not follow the contract we support.`, + ].join(' '), ), }; } else { From a1a70dd7828cd8c0caf3c820fd777a514a535388 Mon Sep 17 00:00:00 2001 From: Yue Guo Date: Wed, 2 Jun 2021 11:39:30 -0700 Subject: [PATCH 17/24] preserve typescript typing. --- src/runtimeHelpers.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/runtimeHelpers.ts b/src/runtimeHelpers.ts index 3cec2bd..c56bf11 100644 --- a/src/runtimeHelpers.ts +++ b/src/runtimeHelpers.ts @@ -359,7 +359,7 @@ export function unPartitionResults( * { bar_id: 2, rating: 3 }, * ] */ -export function unPartitionResultsByBatchKeyPartition( +export function unPartitionResultsByBatchKeyPartition>( newKey: string, propertyBatchKey: string, batchKeyPartition: ReadonlyArray>, @@ -367,7 +367,7 @@ export function unPartitionResultsByBatchKeyPartition( /** Should be a nested array of IDs, as generated by partitionItems */ requestGroups: ReadonlyArray>, /** The results back from the service, in the same shape as groups */ - resultGroups: ReadonlyArray>, + resultGroups: ReadonlyArray>, ): ReadonlyArray { /** * e.g. with our inputs, produce: @@ -405,7 +405,9 @@ export function unPartitionResultsByBatchKeyPartition( resultElement.hasOwnProperty(propertyBatchKey) && resultElement[propertyBatchKey].hasOwnProperty(propertyBatchKeyPartion[i][j]) ) { - result = resultElement; + result = { + ...resultElement, + } as Record; result[propertyBatchKey] = { [propertyBatchKeyPartion[i][j]]: resultElement[propertyBatchKey][propertyBatchKeyPartion[i][j]], From acf6fff4e51c99ca607daf4dc06192967e243b1b Mon Sep 17 00:00:00 2001 From: Yue Guo Date: Wed, 2 Jun 2021 15:59:28 -0700 Subject: [PATCH 18/24] add star war end-to-end test --- examples/swapi/swapi-loaders.js | 616 +++++++++++++++++++- examples/swapi/swapi-server.js | 60 ++ examples/swapi/swapi.dataloader-config.yaml | 7 + examples/swapi/swapi.js | 26 + src/genTypeFlow.ts | 10 + 5 files changed, 707 insertions(+), 12 deletions(-) diff --git a/examples/swapi/swapi-loaders.js b/examples/swapi/swapi-loaders.js index 8e59aeb..0811693 100644 --- a/examples/swapi/swapi-loaders.js +++ b/examples/swapi/swapi-loaders.js @@ -178,6 +178,42 @@ export type LoadersType = $ReadOnly<{| // This third argument is the cache key type. Since we use objectHash in cacheKeyOptions, this is "string". string, >, + getFilmsV2: DataLoader< + {| + ...$Diff< + $Call]>, + { + film_ids: $PropertyType< + $Call]>, + 'film_ids', + >, + properties: $PropertyType< + $Call]>, + 'properties', + >, + }, + >, + ...{| + film_id: $ElementType< + $PropertyType<$Call]>, 'film_ids'>, + 0, + >, + property: $ElementType< + $PropertyType<$Call]>, 'properties'>, + 0, + >, + |}, + |}, + $ElementType< + $Call< + ExtractPromisedReturnValue<[$Call]>]>, + $PropertyType, + >, + 0, + >, + // This third argument is the cache key type. Since we use objectHash in cacheKeyOptions, this is "string". + string, + >, getRoot: DataLoader< $Call]>, $Call< @@ -306,6 +342,20 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade * Returns: * `[ [ 0, 2 ], [ 1 ] ]` * + * We could also have more than one batch key. + * + * Example: + * + * ```js + * partitionItems([ + * { [bar_id: 7, property: 'property_1'], include_extra_info: true }, + * { [bar_id: 8, property: 'property_2'], include_extra_info: false }, + * { [bar_id: 9, property: 'property_3'], include_extra_info: true }, + * ], 'bar_id') + * ``` + * + * Returns: + * `[ [ 0, 2 ], [ 1 ] ]` * We'll refer to each element in the group as a "request ID". */ let requestGroups; @@ -414,6 +464,7 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade 'Add reorderResultsByKey to the config for this resource to be able to handle a partial response.', ].join(' '), ); + // Tell flow that BatchItemNotFoundError extends Error. // It's an issue with flowgen package, but not an issue with Flow. // @see https://github.com/Yelp/dataloader-codegen/pull/35#discussion_r394777533 @@ -462,22 +513,58 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade }), ); + /** + * When there's propertyBatchKey and propertyNewKey, the resource might + * contain less number of items that we requested. We need the value of batchKey and + * propertyBatchKey in requests group to help us split the results back up into the + * order that they were requested. + * + * + * Example: + * + * getBatchKeyForPartitionItems( + * 'bar_id', + * ['bar_id', 'property'], + * [ + * { bar_id: 2, property: 'name', include_extra_info: true }, + * { bar_id: 3, property: 'rating', include_extra_info: false }, + * { bar_id: 4, property: 'rating', include_extra_info: true }, + * ]) + * + * + * Returns: + * [ [ 2, 4 ], [ 3 ] ] + * + * getBatchKeyForPartitionItems( + * 'property', + * ['bar_id', 'property'], + * [ + * { bar_id: 2, property: 'name', include_extra_info: true }, + * { bar_id: 3, property: 'rating', include_extra_info: false }, + * { bar_id: 4, property: 'rating', include_extra_info: true }, + * ]) + * + * + * Returns: + * [ [ 'name', 'rating' ], [ 'rating' ] ] + */ if (false && false) { const batchKeyPartition = getBatchKeysForPartitionItems( 'planet_id', ['planet_id', 'undefined'], keys, ); - const secondaryBatchKeyPartiion = getBatchKeysForPartitionItems( + const propertyBatchKeyPartiion = getBatchKeysForPartitionItems( 'undefined', ['planet_id', 'undefined'], keys, ); - // Split the results back up into the order that they were requested + return unPartitionResultsByBatchKeyPartition( 'planet_id', + 'undefined', batchKeyPartition, - secondaryBatchKeyPartiion, + propertyBatchKeyPartiion, requestGroups, groupedResults, ); @@ -605,6 +692,20 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade * Returns: * `[ [ 0, 2 ], [ 1 ] ]` * + * We could also have more than one batch key. + * + * Example: + * + * ```js + * partitionItems([ + * { [bar_id: 7, property: 'property_1'], include_extra_info: true }, + * { [bar_id: 8, property: 'property_2'], include_extra_info: false }, + * { [bar_id: 9, property: 'property_3'], include_extra_info: true }, + * ], 'bar_id') + * ``` + * + * Returns: + * `[ [ 0, 2 ], [ 1 ] ]` * We'll refer to each element in the group as a "request ID". */ let requestGroups; @@ -710,6 +811,7 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade 'Add reorderResultsByKey to the config for this resource to be able to handle a partial response.', ].join(' '), ); + // Tell flow that BatchItemNotFoundError extends Error. // It's an issue with flowgen package, but not an issue with Flow. // @see https://github.com/Yelp/dataloader-codegen/pull/35#discussion_r394777533 @@ -758,22 +860,58 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade }), ); + /** + * When there's propertyBatchKey and propertyNewKey, the resource might + * contain less number of items that we requested. We need the value of batchKey and + * propertyBatchKey in requests group to help us split the results back up into the + * order that they were requested. + * + * + * Example: + * + * getBatchKeyForPartitionItems( + * 'bar_id', + * ['bar_id', 'property'], + * [ + * { bar_id: 2, property: 'name', include_extra_info: true }, + * { bar_id: 3, property: 'rating', include_extra_info: false }, + * { bar_id: 4, property: 'rating', include_extra_info: true }, + * ]) + * + * + * Returns: + * [ [ 2, 4 ], [ 3 ] ] + * + * getBatchKeyForPartitionItems( + * 'property', + * ['bar_id', 'property'], + * [ + * { bar_id: 2, property: 'name', include_extra_info: true }, + * { bar_id: 3, property: 'rating', include_extra_info: false }, + * { bar_id: 4, property: 'rating', include_extra_info: true }, + * ]) + * + * + * Returns: + * [ [ 'name', 'rating' ], [ 'rating' ] ] + */ if (false && false) { const batchKeyPartition = getBatchKeysForPartitionItems( 'person_id', ['person_id', 'undefined'], keys, ); - const secondaryBatchKeyPartiion = getBatchKeysForPartitionItems( + const propertyBatchKeyPartiion = getBatchKeysForPartitionItems( 'undefined', ['person_id', 'undefined'], keys, ); - // Split the results back up into the order that they were requested + return unPartitionResultsByBatchKeyPartition( 'person_id', + 'undefined', batchKeyPartition, - secondaryBatchKeyPartiion, + propertyBatchKeyPartiion, requestGroups, groupedResults, ); @@ -901,6 +1039,20 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade * Returns: * `[ [ 0, 2 ], [ 1 ] ]` * + * We could also have more than one batch key. + * + * Example: + * + * ```js + * partitionItems([ + * { [bar_id: 7, property: 'property_1'], include_extra_info: true }, + * { [bar_id: 8, property: 'property_2'], include_extra_info: false }, + * { [bar_id: 9, property: 'property_3'], include_extra_info: true }, + * ], 'bar_id') + * ``` + * + * Returns: + * `[ [ 0, 2 ], [ 1 ] ]` * We'll refer to each element in the group as a "request ID". */ let requestGroups; @@ -1009,6 +1161,7 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade 'Add reorderResultsByKey to the config for this resource to be able to handle a partial response.', ].join(' '), ); + // Tell flow that BatchItemNotFoundError extends Error. // It's an issue with flowgen package, but not an issue with Flow. // @see https://github.com/Yelp/dataloader-codegen/pull/35#discussion_r394777533 @@ -1057,22 +1210,58 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade }), ); + /** + * When there's propertyBatchKey and propertyNewKey, the resource might + * contain less number of items that we requested. We need the value of batchKey and + * propertyBatchKey in requests group to help us split the results back up into the + * order that they were requested. + * + * + * Example: + * + * getBatchKeyForPartitionItems( + * 'bar_id', + * ['bar_id', 'property'], + * [ + * { bar_id: 2, property: 'name', include_extra_info: true }, + * { bar_id: 3, property: 'rating', include_extra_info: false }, + * { bar_id: 4, property: 'rating', include_extra_info: true }, + * ]) + * + * + * Returns: + * [ [ 2, 4 ], [ 3 ] ] + * + * getBatchKeyForPartitionItems( + * 'property', + * ['bar_id', 'property'], + * [ + * { bar_id: 2, property: 'name', include_extra_info: true }, + * { bar_id: 3, property: 'rating', include_extra_info: false }, + * { bar_id: 4, property: 'rating', include_extra_info: true }, + * ]) + * + * + * Returns: + * [ [ 'name', 'rating' ], [ 'rating' ] ] + */ if (false && false) { const batchKeyPartition = getBatchKeysForPartitionItems( 'vehicle_id', ['vehicle_id', 'undefined'], keys, ); - const secondaryBatchKeyPartiion = getBatchKeysForPartitionItems( + const propertyBatchKeyPartiion = getBatchKeysForPartitionItems( 'undefined', ['vehicle_id', 'undefined'], keys, ); - // Split the results back up into the order that they were requested + return unPartitionResultsByBatchKeyPartition( 'vehicle_id', + 'undefined', batchKeyPartition, - secondaryBatchKeyPartiion, + propertyBatchKeyPartiion, requestGroups, groupedResults, ); @@ -1209,9 +1398,28 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade * Returns: * `[ [ 0, 2 ], [ 1 ] ]` * + * We could also have more than one batch key. + * + * Example: + * + * ```js + * partitionItems([ + * { [bar_id: 7, property: 'property_1'], include_extra_info: true }, + * { [bar_id: 8, property: 'property_2'], include_extra_info: false }, + * { [bar_id: 9, property: 'property_3'], include_extra_info: true }, + * ], 'bar_id') + * ``` + * + * Returns: + * `[ [ 0, 2 ], [ 1 ] ]` * We'll refer to each element in the group as a "request ID". */ - const requestGroups = partitionItems('film_id', keys); + let requestGroups; + if (false && false) { + requestGroups = partitionItems(['film_id', 'undefined'], keys); + } else { + requestGroups = partitionItems('film_id', keys); + } // Map the request groups to a list of Promises - one for each request const groupedResults = await Promise.all( @@ -1356,8 +1564,392 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade }), ); - // Split the results back up into the order that they were requested - return unPartitionResults(requestGroups, groupedResults); + /** + * When there's propertyBatchKey and propertyNewKey, the resource might + * contain less number of items that we requested. We need the value of batchKey and + * propertyBatchKey in requests group to help us split the results back up into the + * order that they were requested. + * + * + * Example: + * + * getBatchKeyForPartitionItems( + * 'bar_id', + * ['bar_id', 'property'], + * [ + * { bar_id: 2, property: 'name', include_extra_info: true }, + * { bar_id: 3, property: 'rating', include_extra_info: false }, + * { bar_id: 4, property: 'rating', include_extra_info: true }, + * ]) + * + * + * Returns: + * [ [ 2, 4 ], [ 3 ] ] + * + * getBatchKeyForPartitionItems( + * 'property', + * ['bar_id', 'property'], + * [ + * { bar_id: 2, property: 'name', include_extra_info: true }, + * { bar_id: 3, property: 'rating', include_extra_info: false }, + * { bar_id: 4, property: 'rating', include_extra_info: true }, + * ]) + * + * + * Returns: + * [ [ 'name', 'rating' ], [ 'rating' ] ] + */ + if (false && false) { + const batchKeyPartition = getBatchKeysForPartitionItems('film_id', ['film_id', 'undefined'], keys); + const propertyBatchKeyPartiion = getBatchKeysForPartitionItems( + 'undefined', + ['film_id', 'undefined'], + keys, + ); + + return unPartitionResultsByBatchKeyPartition( + 'film_id', + 'undefined', + batchKeyPartition, + propertyBatchKeyPartiion, + requestGroups, + groupedResults, + ); + } else { + // Split the results back up into the order that they were requested + return unPartitionResults(requestGroups, groupedResults); + } + }, + { + ...cacheKeyOptions, + }, + ), + getFilmsV2: new DataLoader< + {| + ...$Diff< + $Call]>, + { + film_ids: $PropertyType< + $Call]>, + 'film_ids', + >, + properties: $PropertyType< + $Call]>, + 'properties', + >, + }, + >, + ...{| + film_id: $ElementType< + $PropertyType<$Call]>, 'film_ids'>, + 0, + >, + property: $ElementType< + $PropertyType<$Call]>, 'properties'>, + 0, + >, + |}, + |}, + $ElementType< + $Call< + ExtractPromisedReturnValue<[$Call]>]>, + $PropertyType, + >, + 0, + >, + // This third argument is the cache key type. Since we use objectHash in cacheKeyOptions, this is "string". + string, + >( + /** + * =============================================================== + * Generated DataLoader: getFilmsV2 + * =============================================================== + * + * Resource Config: + * + * ```json + * { + * "docsLink": "https://swapi.dev/documentation#films", + * "isBatchResource": true, + * "batchKey": "film_ids", + * "newKey": "film_id", + * "propertyBatchKey": "properties", + * "propertyNewKey": "property" + * } + * ``` + */ + async (keys) => { + invariant( + typeof resources.getFilmsV2 === 'function', + [ + '[dataloader-codegen :: getFilmsV2] resources.getFilmsV2 is not a function.', + 'Did you pass in an instance of getFilmsV2 to "getLoaders"?', + ].join(' '), + ); + + /** + * Chunk up the "keys" array to create a set of "request groups". + * + * We're about to hit a batch resource. In addition to the batch + * key, the resource may take other arguments too. When batching + * up requests, we'll want to look out for where those other + * arguments differ, and send multiple requests so we don't get + * back the wrong info. + * + * In other words, we'll potentially want to send _multiple_ + * requests to the underlying resource batch method in this + * dataloader body. + * + * ~~~ Why? ~~~ + * + * Consider what happens when we get called with arguments where + * the non-batch keys differ. + * + * Example: + * + * ```js + * loaders.foo.load({ foo_id: 2, include_private_data: true }); + * loaders.foo.load({ foo_id: 3, include_private_data: false }); + * loaders.foo.load({ foo_id: 4, include_private_data: false }); + * ``` + * + * If we collected everything up and tried to send the one + * request to the resource as a batch request, how do we know + * what the value for "include_private_data" should be? We're + * going to have to group these up up and send two requests to + * the resource to make sure we're requesting the right stuff. + * + * e.g. We'd need to make the following set of underlying resource + * calls: + * + * ```js + * foo({ foo_ids: [ 2 ], include_private_data: true }); + * foo({ foo_ids: [ 3, 4 ], include_private_data: false }); + * ``` + * + * ~~~ tl;dr ~~~ + * + * When we have calls to .load with differing non batch key args, + * we'll need to send multiple requests to the underlying + * resource to make sure we get the right results back. + * + * Let's create the request groups, where each element in the + * group refers to a position in "keys" (i.e. a call to .load) + * + * Example: + * + * ```js + * partitionItems([ + * { bar_id: 7, include_extra_info: true }, + * { bar_id: 8, include_extra_info: false }, + * { bar_id: 9, include_extra_info: true }, + * ], 'bar_id') + * ``` + * + * Returns: + * `[ [ 0, 2 ], [ 1 ] ]` + * + * We could also have more than one batch key. + * + * Example: + * + * ```js + * partitionItems([ + * { [bar_id: 7, property: 'property_1'], include_extra_info: true }, + * { [bar_id: 8, property: 'property_2'], include_extra_info: false }, + * { [bar_id: 9, property: 'property_3'], include_extra_info: true }, + * ], 'bar_id') + * ``` + * + * Returns: + * `[ [ 0, 2 ], [ 1 ] ]` + * We'll refer to each element in the group as a "request ID". + */ + let requestGroups; + if (true && true) { + requestGroups = partitionItems(['film_id', 'property'], keys); + } else { + requestGroups = partitionItems('film_id', keys); + } + + // Map the request groups to a list of Promises - one for each request + const groupedResults = await Promise.all( + requestGroups.map(async (requestIDs) => { + /** + * Select a set of elements in "keys", where all non-batch + * keys should be identical. + * + * We're going to smoosh all these together into one payload to + * send to the resource as a batch request! + */ + const requests = requestIDs.map((id) => keys[id]); + + // For now, we assume that the dataloader key should be the first argument to the resource + // @see https://github.com/Yelp/dataloader-codegen/issues/56 + const resourceArgs = [ + { + ..._.omit(requests[0], 'film_id', 'property'), + ['film_ids']: requests.map((k) => k['film_id']), + ['properties']: requests.map((k) => k['property']), + }, + ]; + + let response = await (async (_resourceArgs) => { + // Make a re-assignable variable so flow/eslint doesn't complain + let __resourceArgs = _resourceArgs; + + if (options && options.resourceMiddleware && options.resourceMiddleware.before) { + __resourceArgs = await options.resourceMiddleware.before( + ['getFilmsV2'], + __resourceArgs, + ); + } + + let _response; + try { + // Finally, call the resource! + _response = await resources.getFilmsV2(...__resourceArgs); + } catch (error) { + const errorHandler = + options && typeof options.errorHandler === 'function' + ? options.errorHandler + : defaultErrorHandler; + + /** + * Apply some error handling to catch and handle all errors/rejected promises. errorHandler must return an Error object. + * + * If we let errors here go unhandled here, it will bubble up and DataLoader will return an error for all + * keys requested. We can do slightly better by returning the error object for just the keys in this batch request. + */ + _response = await errorHandler(['getFilmsV2'], error); + + // Check that errorHandler actually returned an Error object, and turn it into one if not. + if (!(_response instanceof Error)) { + _response = new Error( + [ + `[dataloader-codegen :: getFilmsV2] Caught an error, but errorHandler did not return an Error object.`, + `Instead, got ${typeof _response}: ${util.inspect(_response)}`, + ].join(' '), + ); + } + } + + if (options && options.resourceMiddleware && options.resourceMiddleware.after) { + _response = await options.resourceMiddleware.after(['getFilmsV2'], _response); + } + + return _response; + })(resourceArgs); + + if (!(response instanceof Error)) { + } + + if (!(response instanceof Error)) { + if (!Array.isArray(response)) { + response = new Error( + ['[dataloader-codegen :: getFilmsV2]', 'Expected response to be an array!'].join( + ' ', + ), + ); + } + } + + /** + * If the resource returns an Error, we'll want to copy and + * return that error as the return value for every request in + * this group. + * + * This allow the error to be cached, and allows the rest of the + * requests made by this DataLoader to succeed. + * + * @see https://github.com/graphql/dataloader#caching-errors + */ + if (response instanceof Error) { + response = requestIDs.map((requestId) => { + /** + * Since we're returning an error object and not the + * expected return type from the resource, this element + * would be unsortable, since it wouldn't have the + * "reorderResultsByKey" attribute. + * + * Let's add it to the error object, as "reorderResultsByValue". + * + * (If we didn't specify that this resource needs + * sorting, then this will be "null" and won't be used.) + */ + const reorderResultsByValue = null; + + // Tell flow that "response" is actually an error object. + // (This is so we can pass it as 'cause' to CaughtResourceError) + invariant(response instanceof Error, 'expected response to be an error'); + + return new CaughtResourceError( + `[dataloader-codegen :: getFilmsV2] Caught error during call to resource. Error: ${response.stack}`, + response, + reorderResultsByValue, + ); + }); + } + + return response; + }), + ); + + /** + * When there's propertyBatchKey and propertyNewKey, the resource might + * contain less number of items that we requested. We need the value of batchKey and + * propertyBatchKey in requests group to help us split the results back up into the + * order that they were requested. + * + * + * Example: + * + * getBatchKeyForPartitionItems( + * 'bar_id', + * ['bar_id', 'property'], + * [ + * { bar_id: 2, property: 'name', include_extra_info: true }, + * { bar_id: 3, property: 'rating', include_extra_info: false }, + * { bar_id: 4, property: 'rating', include_extra_info: true }, + * ]) + * + * + * Returns: + * [ [ 2, 4 ], [ 3 ] ] + * + * getBatchKeyForPartitionItems( + * 'property', + * ['bar_id', 'property'], + * [ + * { bar_id: 2, property: 'name', include_extra_info: true }, + * { bar_id: 3, property: 'rating', include_extra_info: false }, + * { bar_id: 4, property: 'rating', include_extra_info: true }, + * ]) + * + * + * Returns: + * [ [ 'name', 'rating' ], [ 'rating' ] ] + */ + if (true && true) { + const batchKeyPartition = getBatchKeysForPartitionItems('film_id', ['film_id', 'property'], keys); + const propertyBatchKeyPartiion = getBatchKeysForPartitionItems( + 'property', + ['film_id', 'property'], + keys, + ); + + return unPartitionResultsByBatchKeyPartition( + 'film_id', + 'properties', + batchKeyPartition, + propertyBatchKeyPartiion, + requestGroups, + groupedResults, + ); + } else { + // Split the results back up into the order that they were requested + return unPartitionResults(requestGroups, groupedResults); + } }, { ...cacheKeyOptions, diff --git a/examples/swapi/swapi-server.js b/examples/swapi/swapi-server.js index ed63433..43efb6a 100644 --- a/examples/swapi/swapi-server.js +++ b/examples/swapi/swapi-server.js @@ -20,9 +20,16 @@ const createSWAPIServer = () => { director: String } + type FilmV2 { + title: String + episodeNumber: Int + director: String + } + type Query { planet(id: Int): Planet film(id: Int): Film + filmv2(id: Int): FilmV2 } `); @@ -114,6 +121,50 @@ const createSWAPIServer = () => { } } + class FilmModelV2 { + id: number; + + constructor(id: number) { + this.id = id; + } + + async title() { + const response = await swapiLoaders.getFilmsV2.load({ film_id: this.id, property: 'title' }); + + if (response instanceof Error) { + return response; + } + + if (response) { + return response.title; + } + } + + async episodeNumber() { + const response = await swapiLoaders.getFilmsV2.load({ film_id: this.id, property: 'episode_id' }); + + if (response instanceof Error) { + return response; + } + + if (response) { + return response.episode_id; + } + } + + async director() { + const response = await swapiLoaders.getFilmsV2.load({ film_id: this.id, property: 'director' }); + + if (response instanceof Error) { + return response; + } + + if (response) { + return response.director; + } + } + } + const root = { planet: ({ id }) => { return new PlanetModel(id); @@ -121,6 +172,9 @@ const createSWAPIServer = () => { film: ({ id }) => { return new FilmModel(id); }, + filmv2: ({ id }) => { + return new FilmModelV2(id); + }, }; return { schema, root }; @@ -155,6 +209,12 @@ runQuery(/* GraphQL */ ` episodeNumber director } + + theBestV2: filmv2(id: 4) { + title + episodeNumber + director + } } `).then((result) => { console.log(JSON.stringify(result, null, 4)); diff --git a/examples/swapi/swapi.dataloader-config.yaml b/examples/swapi/swapi.dataloader-config.yaml index 6e35f3a..e8376a1 100644 --- a/examples/swapi/swapi.dataloader-config.yaml +++ b/examples/swapi/swapi.dataloader-config.yaml @@ -28,6 +28,13 @@ resources: batchKey: film_ids newKey: film_id isBatchKeyASet: true + getFilmsV2: + docsLink: https://swapi.dev/documentation#films + isBatchResource: true + batchKey: film_ids + newKey: film_id + propertyBatchKey: properties + propertyNewKey: property getRoot: docsLink: https://swapi.dev/documentation#root isBatchResource: false diff --git a/examples/swapi/swapi.js b/examples/swapi/swapi.js index 83945f2..cd3a6eb 100644 --- a/examples/swapi/swapi.js +++ b/examples/swapi/swapi.js @@ -3,6 +3,9 @@ * @flow */ +import { property } from 'lodash'; +import { number } from 'yargs'; + const url = require('url'); const fetch = require('node-fetch').default; const SWAPI_URL = 'https://swapi.dev/api/'; @@ -60,6 +63,14 @@ export type SWAPI_Film = $ReadOnly<{| edited: string, |}>; +export type SWAPI_Film_v2 = $ReadOnly<{| + film_id: number, + title: string, + episode_id: number, + director: string, + producer: string, +|}>; + export type SWAPI_Vehicle = $ReadOnly<{| name: string, key: string, @@ -79,6 +90,10 @@ export type SWAPIClientlibTypes = {| getPeople: ({| people_ids: $ReadOnlyArray |}) => Promise<$ReadOnlyArray>, getVehicles: ({| vehicle_ids: $ReadOnlyArray |}) => Promise<$ReadOnlyArray>, getFilms: ({| film_ids: Set |}) => Promise<$ReadOnlyArray>, + getFilmsV2: ({| + film_ids: $ReadOnlyArray, + properties: $ReadOnlyArray, + |}) => $ReadOnlyArray, getRoot: ({||}) => Promise, |}; @@ -100,6 +115,17 @@ module.exports = function (): SWAPIClientlibTypes { Promise.all( [...film_ids].map((id) => fetch(url.resolve(SWAPI_URL, `films/${id}`)).then((res) => res.json())), ), + getFilmsV2: ({ film_ids, properties }) => { + return [ + { + film_id: 4, + director: 'George Lucas', + producer: 'Rick McCallum', + episode_id: 1, + title: 'The Phantom Menace', + }, + ]; + }, getRoot: ({}) => fetch(SWAPI_URL).then((res) => res.json()), }; }; diff --git a/src/genTypeFlow.ts b/src/genTypeFlow.ts index 8b0cbf3..f91bf50 100644 --- a/src/genTypeFlow.ts +++ b/src/genTypeFlow.ts @@ -68,6 +68,16 @@ export function getLoaderTypeKey(resourceConfig: ResourceConfig, resourcePath: R )}`; } + if (typeof resourceConfig.propertyNewKey === 'string' && typeof resourceConfig.propertyBatchKey === 'string') { + return `{| + ...$Diff<${resourceArgs}, { + ${resourceConfig.batchKey}: $PropertyType<${resourceArgs}, '${resourceConfig.batchKey}'>, + ${resourceConfig.propertyBatchKey}: $PropertyType<${resourceArgs}, '${resourceConfig.propertyBatchKey}'> + }>, + ...{| ${newKeyType}, ${resourceConfig.propertyNewKey}: $ElementType<$PropertyType<${resourceArgs}, '${resourceConfig.propertyBatchKey}'>, 0> |}, + |}`; + } + return `{| ...$Diff<${resourceArgs}, { ${resourceConfig.batchKey}: $PropertyType<${resourceArgs}, '${resourceConfig.batchKey}'> From bf485de159e250ba6c321231c8ec052754477b57 Mon Sep 17 00:00:00 2001 From: Yue Guo Date: Fri, 4 Jun 2021 13:20:56 -0700 Subject: [PATCH 19/24] add e2e tests for all three cases --- examples/swapi/swapi-loaders.js | 791 +++++++++++++++++++- examples/swapi/swapi-server.js | 117 ++- examples/swapi/swapi.dataloader-config.yaml | 15 + examples/swapi/swapi.js | 66 +- 4 files changed, 968 insertions(+), 21 deletions(-) diff --git a/examples/swapi/swapi-loaders.js b/examples/swapi/swapi-loaders.js index 0811693..6f2c6a6 100644 --- a/examples/swapi/swapi-loaders.js +++ b/examples/swapi/swapi-loaders.js @@ -204,10 +204,85 @@ export type LoadersType = $ReadOnly<{| >, |}, |}, + $PropertyType< + $ElementType< + $Call< + ExtractPromisedReturnValue<[$Call]>]>, + $PropertyType, + >, + 0, + >, + 'properties', + >, + // This third argument is the cache key type. Since we use objectHash in cacheKeyOptions, this is "string". + string, + >, + getFilmsV3: DataLoader< + {| + ...$Diff< + $Call]>, + { + film_ids: $PropertyType< + $Call]>, + 'film_ids', + >, + properties: $PropertyType< + $Call]>, + 'properties', + >, + }, + >, + ...{| + film_id: $ElementType< + $PropertyType<$Call]>, 'film_ids'>, + 0, + >, + property: $ElementType< + $PropertyType<$Call]>, 'properties'>, + 0, + >, + |}, + |}, + $ElementType< + $Call< + ExtractPromisedReturnValue<[$Call]>]>, + $PropertyType, + >, + 0, + >, + // This third argument is the cache key type. Since we use objectHash in cacheKeyOptions, this is "string". + string, + >, + getFilmsV4: DataLoader< + {| + ...$Diff< + $Call]>, + { + film_ids: $PropertyType< + $Call]>, + 'film_ids', + >, + properties: $PropertyType< + $Call]>, + 'properties', + >, + }, + >, + ...{| + film_id: $ElementType< + $PropertyType<$Call]>, 'film_ids'>, + 0, + >, + property: $ElementType< + $PropertyType<$Call]>, 'properties'>, + 0, + >, + |}, + |}, $ElementType< $Call< - ExtractPromisedReturnValue<[$Call]>]>, - $PropertyType, + ExtractPromisedReturnValue<[$Call]>]>, + $PropertyType, >, 0, >, @@ -1650,12 +1725,15 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade >, |}, |}, - $ElementType< - $Call< - ExtractPromisedReturnValue<[$Call]>]>, - $PropertyType, + $PropertyType< + $ElementType< + $Call< + ExtractPromisedReturnValue<[$Call]>]>, + $PropertyType, + >, + 0, >, - 0, + 'properties', >, // This third argument is the cache key type. Since we use objectHash in cacheKeyOptions, this is "string". string, @@ -1674,7 +1752,8 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade * "batchKey": "film_ids", * "newKey": "film_id", * "propertyBatchKey": "properties", - * "propertyNewKey": "property" + * "propertyNewKey": "property", + * "nestedPath": "properties" * } * ``` */ @@ -1842,6 +1921,40 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade })(resourceArgs); if (!(response instanceof Error)) { + /** + * Un-nest the actual data from the resource return value. + * + * e.g. + * ```js + * { + * foos: [ + * { id: 1, value: 'hello' }, + * { id: 2, value: 'world' }, + * ] + * } + * ``` + * + * Becomes + * + * ```js + * [ + * { id: 1, value: 'hello' }, + * { id: 2, value: 'world' }, + * ] + * ``` + */ + response = _.get( + response, + 'properties', + new Error( + [ + '[dataloader-codegen :: getFilmsV2]', + 'Tried to un-nest the response from the resource, but', + ".get(response, 'properties')", + 'was empty!', + ].join(' '), + ), + ); } if (!(response instanceof Error)) { @@ -1955,6 +2068,668 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade ...cacheKeyOptions, }, ), + getFilmsV3: new DataLoader< + {| + ...$Diff< + $Call]>, + { + film_ids: $PropertyType< + $Call]>, + 'film_ids', + >, + properties: $PropertyType< + $Call]>, + 'properties', + >, + }, + >, + ...{| + film_id: $ElementType< + $PropertyType<$Call]>, 'film_ids'>, + 0, + >, + property: $ElementType< + $PropertyType<$Call]>, 'properties'>, + 0, + >, + |}, + |}, + $ElementType< + $Call< + ExtractPromisedReturnValue<[$Call]>]>, + $PropertyType, + >, + 0, + >, + // This third argument is the cache key type. Since we use objectHash in cacheKeyOptions, this is "string". + string, + >( + /** + * =============================================================== + * Generated DataLoader: getFilmsV3 + * =============================================================== + * + * Resource Config: + * + * ```json + * { + * "docsLink": "https://swapi.dev/documentation#films", + * "isBatchResource": true, + * "batchKey": "film_ids", + * "newKey": "film_id", + * "propertyBatchKey": "properties", + * "propertyNewKey": "property" + * } + * ``` + */ + async (keys) => { + invariant( + typeof resources.getFilmsV3 === 'function', + [ + '[dataloader-codegen :: getFilmsV3] resources.getFilmsV3 is not a function.', + 'Did you pass in an instance of getFilmsV3 to "getLoaders"?', + ].join(' '), + ); + + /** + * Chunk up the "keys" array to create a set of "request groups". + * + * We're about to hit a batch resource. In addition to the batch + * key, the resource may take other arguments too. When batching + * up requests, we'll want to look out for where those other + * arguments differ, and send multiple requests so we don't get + * back the wrong info. + * + * In other words, we'll potentially want to send _multiple_ + * requests to the underlying resource batch method in this + * dataloader body. + * + * ~~~ Why? ~~~ + * + * Consider what happens when we get called with arguments where + * the non-batch keys differ. + * + * Example: + * + * ```js + * loaders.foo.load({ foo_id: 2, include_private_data: true }); + * loaders.foo.load({ foo_id: 3, include_private_data: false }); + * loaders.foo.load({ foo_id: 4, include_private_data: false }); + * ``` + * + * If we collected everything up and tried to send the one + * request to the resource as a batch request, how do we know + * what the value for "include_private_data" should be? We're + * going to have to group these up up and send two requests to + * the resource to make sure we're requesting the right stuff. + * + * e.g. We'd need to make the following set of underlying resource + * calls: + * + * ```js + * foo({ foo_ids: [ 2 ], include_private_data: true }); + * foo({ foo_ids: [ 3, 4 ], include_private_data: false }); + * ``` + * + * ~~~ tl;dr ~~~ + * + * When we have calls to .load with differing non batch key args, + * we'll need to send multiple requests to the underlying + * resource to make sure we get the right results back. + * + * Let's create the request groups, where each element in the + * group refers to a position in "keys" (i.e. a call to .load) + * + * Example: + * + * ```js + * partitionItems([ + * { bar_id: 7, include_extra_info: true }, + * { bar_id: 8, include_extra_info: false }, + * { bar_id: 9, include_extra_info: true }, + * ], 'bar_id') + * ``` + * + * Returns: + * `[ [ 0, 2 ], [ 1 ] ]` + * + * We could also have more than one batch key. + * + * Example: + * + * ```js + * partitionItems([ + * { [bar_id: 7, property: 'property_1'], include_extra_info: true }, + * { [bar_id: 8, property: 'property_2'], include_extra_info: false }, + * { [bar_id: 9, property: 'property_3'], include_extra_info: true }, + * ], 'bar_id') + * ``` + * + * Returns: + * `[ [ 0, 2 ], [ 1 ] ]` + * We'll refer to each element in the group as a "request ID". + */ + let requestGroups; + if (true && true) { + requestGroups = partitionItems(['film_id', 'property'], keys); + } else { + requestGroups = partitionItems('film_id', keys); + } + + // Map the request groups to a list of Promises - one for each request + const groupedResults = await Promise.all( + requestGroups.map(async (requestIDs) => { + /** + * Select a set of elements in "keys", where all non-batch + * keys should be identical. + * + * We're going to smoosh all these together into one payload to + * send to the resource as a batch request! + */ + const requests = requestIDs.map((id) => keys[id]); + + // For now, we assume that the dataloader key should be the first argument to the resource + // @see https://github.com/Yelp/dataloader-codegen/issues/56 + const resourceArgs = [ + { + ..._.omit(requests[0], 'film_id', 'property'), + ['film_ids']: requests.map((k) => k['film_id']), + ['properties']: requests.map((k) => k['property']), + }, + ]; + + let response = await (async (_resourceArgs) => { + // Make a re-assignable variable so flow/eslint doesn't complain + let __resourceArgs = _resourceArgs; + + if (options && options.resourceMiddleware && options.resourceMiddleware.before) { + __resourceArgs = await options.resourceMiddleware.before( + ['getFilmsV3'], + __resourceArgs, + ); + } + + let _response; + try { + // Finally, call the resource! + _response = await resources.getFilmsV3(...__resourceArgs); + } catch (error) { + const errorHandler = + options && typeof options.errorHandler === 'function' + ? options.errorHandler + : defaultErrorHandler; + + /** + * Apply some error handling to catch and handle all errors/rejected promises. errorHandler must return an Error object. + * + * If we let errors here go unhandled here, it will bubble up and DataLoader will return an error for all + * keys requested. We can do slightly better by returning the error object for just the keys in this batch request. + */ + _response = await errorHandler(['getFilmsV3'], error); + + // Check that errorHandler actually returned an Error object, and turn it into one if not. + if (!(_response instanceof Error)) { + _response = new Error( + [ + `[dataloader-codegen :: getFilmsV3] Caught an error, but errorHandler did not return an Error object.`, + `Instead, got ${typeof _response}: ${util.inspect(_response)}`, + ].join(' '), + ); + } + } + + if (options && options.resourceMiddleware && options.resourceMiddleware.after) { + _response = await options.resourceMiddleware.after(['getFilmsV3'], _response); + } + + return _response; + })(resourceArgs); + + if (!(response instanceof Error)) { + } + + if (!(response instanceof Error)) { + if (!Array.isArray(response)) { + response = new Error( + ['[dataloader-codegen :: getFilmsV3]', 'Expected response to be an array!'].join( + ' ', + ), + ); + } + } + + /** + * If the resource returns an Error, we'll want to copy and + * return that error as the return value for every request in + * this group. + * + * This allow the error to be cached, and allows the rest of the + * requests made by this DataLoader to succeed. + * + * @see https://github.com/graphql/dataloader#caching-errors + */ + if (response instanceof Error) { + response = requestIDs.map((requestId) => { + /** + * Since we're returning an error object and not the + * expected return type from the resource, this element + * would be unsortable, since it wouldn't have the + * "reorderResultsByKey" attribute. + * + * Let's add it to the error object, as "reorderResultsByValue". + * + * (If we didn't specify that this resource needs + * sorting, then this will be "null" and won't be used.) + */ + const reorderResultsByValue = null; + + // Tell flow that "response" is actually an error object. + // (This is so we can pass it as 'cause' to CaughtResourceError) + invariant(response instanceof Error, 'expected response to be an error'); + + return new CaughtResourceError( + `[dataloader-codegen :: getFilmsV3] Caught error during call to resource. Error: ${response.stack}`, + response, + reorderResultsByValue, + ); + }); + } + + return response; + }), + ); + + /** + * When there's propertyBatchKey and propertyNewKey, the resource might + * contain less number of items that we requested. We need the value of batchKey and + * propertyBatchKey in requests group to help us split the results back up into the + * order that they were requested. + * + * + * Example: + * + * getBatchKeyForPartitionItems( + * 'bar_id', + * ['bar_id', 'property'], + * [ + * { bar_id: 2, property: 'name', include_extra_info: true }, + * { bar_id: 3, property: 'rating', include_extra_info: false }, + * { bar_id: 4, property: 'rating', include_extra_info: true }, + * ]) + * + * + * Returns: + * [ [ 2, 4 ], [ 3 ] ] + * + * getBatchKeyForPartitionItems( + * 'property', + * ['bar_id', 'property'], + * [ + * { bar_id: 2, property: 'name', include_extra_info: true }, + * { bar_id: 3, property: 'rating', include_extra_info: false }, + * { bar_id: 4, property: 'rating', include_extra_info: true }, + * ]) + * + * + * Returns: + * [ [ 'name', 'rating' ], [ 'rating' ] ] + */ + if (true && true) { + const batchKeyPartition = getBatchKeysForPartitionItems('film_id', ['film_id', 'property'], keys); + const propertyBatchKeyPartiion = getBatchKeysForPartitionItems( + 'property', + ['film_id', 'property'], + keys, + ); + + return unPartitionResultsByBatchKeyPartition( + 'film_id', + 'properties', + batchKeyPartition, + propertyBatchKeyPartiion, + requestGroups, + groupedResults, + ); + } else { + // Split the results back up into the order that they were requested + return unPartitionResults(requestGroups, groupedResults); + } + }, + { + ...cacheKeyOptions, + }, + ), + getFilmsV4: new DataLoader< + {| + ...$Diff< + $Call]>, + { + film_ids: $PropertyType< + $Call]>, + 'film_ids', + >, + properties: $PropertyType< + $Call]>, + 'properties', + >, + }, + >, + ...{| + film_id: $ElementType< + $PropertyType<$Call]>, 'film_ids'>, + 0, + >, + property: $ElementType< + $PropertyType<$Call]>, 'properties'>, + 0, + >, + |}, + |}, + $ElementType< + $Call< + ExtractPromisedReturnValue<[$Call]>]>, + $PropertyType, + >, + 0, + >, + // This third argument is the cache key type. Since we use objectHash in cacheKeyOptions, this is "string". + string, + >( + /** + * =============================================================== + * Generated DataLoader: getFilmsV4 + * =============================================================== + * + * Resource Config: + * + * ```json + * { + * "docsLink": "https://swapi.dev/documentation#films", + * "isBatchResource": true, + * "batchKey": "film_ids", + * "newKey": "film_id", + * "propertyBatchKey": "properties", + * "propertyNewKey": "property" + * } + * ``` + */ + async (keys) => { + invariant( + typeof resources.getFilmsV4 === 'function', + [ + '[dataloader-codegen :: getFilmsV4] resources.getFilmsV4 is not a function.', + 'Did you pass in an instance of getFilmsV4 to "getLoaders"?', + ].join(' '), + ); + + /** + * Chunk up the "keys" array to create a set of "request groups". + * + * We're about to hit a batch resource. In addition to the batch + * key, the resource may take other arguments too. When batching + * up requests, we'll want to look out for where those other + * arguments differ, and send multiple requests so we don't get + * back the wrong info. + * + * In other words, we'll potentially want to send _multiple_ + * requests to the underlying resource batch method in this + * dataloader body. + * + * ~~~ Why? ~~~ + * + * Consider what happens when we get called with arguments where + * the non-batch keys differ. + * + * Example: + * + * ```js + * loaders.foo.load({ foo_id: 2, include_private_data: true }); + * loaders.foo.load({ foo_id: 3, include_private_data: false }); + * loaders.foo.load({ foo_id: 4, include_private_data: false }); + * ``` + * + * If we collected everything up and tried to send the one + * request to the resource as a batch request, how do we know + * what the value for "include_private_data" should be? We're + * going to have to group these up up and send two requests to + * the resource to make sure we're requesting the right stuff. + * + * e.g. We'd need to make the following set of underlying resource + * calls: + * + * ```js + * foo({ foo_ids: [ 2 ], include_private_data: true }); + * foo({ foo_ids: [ 3, 4 ], include_private_data: false }); + * ``` + * + * ~~~ tl;dr ~~~ + * + * When we have calls to .load with differing non batch key args, + * we'll need to send multiple requests to the underlying + * resource to make sure we get the right results back. + * + * Let's create the request groups, where each element in the + * group refers to a position in "keys" (i.e. a call to .load) + * + * Example: + * + * ```js + * partitionItems([ + * { bar_id: 7, include_extra_info: true }, + * { bar_id: 8, include_extra_info: false }, + * { bar_id: 9, include_extra_info: true }, + * ], 'bar_id') + * ``` + * + * Returns: + * `[ [ 0, 2 ], [ 1 ] ]` + * + * We could also have more than one batch key. + * + * Example: + * + * ```js + * partitionItems([ + * { [bar_id: 7, property: 'property_1'], include_extra_info: true }, + * { [bar_id: 8, property: 'property_2'], include_extra_info: false }, + * { [bar_id: 9, property: 'property_3'], include_extra_info: true }, + * ], 'bar_id') + * ``` + * + * Returns: + * `[ [ 0, 2 ], [ 1 ] ]` + * We'll refer to each element in the group as a "request ID". + */ + let requestGroups; + if (true && true) { + requestGroups = partitionItems(['film_id', 'property'], keys); + } else { + requestGroups = partitionItems('film_id', keys); + } + + // Map the request groups to a list of Promises - one for each request + const groupedResults = await Promise.all( + requestGroups.map(async (requestIDs) => { + /** + * Select a set of elements in "keys", where all non-batch + * keys should be identical. + * + * We're going to smoosh all these together into one payload to + * send to the resource as a batch request! + */ + const requests = requestIDs.map((id) => keys[id]); + + // For now, we assume that the dataloader key should be the first argument to the resource + // @see https://github.com/Yelp/dataloader-codegen/issues/56 + const resourceArgs = [ + { + ..._.omit(requests[0], 'film_id', 'property'), + ['film_ids']: requests.map((k) => k['film_id']), + ['properties']: requests.map((k) => k['property']), + }, + ]; + + let response = await (async (_resourceArgs) => { + // Make a re-assignable variable so flow/eslint doesn't complain + let __resourceArgs = _resourceArgs; + + if (options && options.resourceMiddleware && options.resourceMiddleware.before) { + __resourceArgs = await options.resourceMiddleware.before( + ['getFilmsV4'], + __resourceArgs, + ); + } + + let _response; + try { + // Finally, call the resource! + _response = await resources.getFilmsV4(...__resourceArgs); + } catch (error) { + const errorHandler = + options && typeof options.errorHandler === 'function' + ? options.errorHandler + : defaultErrorHandler; + + /** + * Apply some error handling to catch and handle all errors/rejected promises. errorHandler must return an Error object. + * + * If we let errors here go unhandled here, it will bubble up and DataLoader will return an error for all + * keys requested. We can do slightly better by returning the error object for just the keys in this batch request. + */ + _response = await errorHandler(['getFilmsV4'], error); + + // Check that errorHandler actually returned an Error object, and turn it into one if not. + if (!(_response instanceof Error)) { + _response = new Error( + [ + `[dataloader-codegen :: getFilmsV4] Caught an error, but errorHandler did not return an Error object.`, + `Instead, got ${typeof _response}: ${util.inspect(_response)}`, + ].join(' '), + ); + } + } + + if (options && options.resourceMiddleware && options.resourceMiddleware.after) { + _response = await options.resourceMiddleware.after(['getFilmsV4'], _response); + } + + return _response; + })(resourceArgs); + + if (!(response instanceof Error)) { + } + + if (!(response instanceof Error)) { + if (!Array.isArray(response)) { + response = new Error( + ['[dataloader-codegen :: getFilmsV4]', 'Expected response to be an array!'].join( + ' ', + ), + ); + } + } + + /** + * If the resource returns an Error, we'll want to copy and + * return that error as the return value for every request in + * this group. + * + * This allow the error to be cached, and allows the rest of the + * requests made by this DataLoader to succeed. + * + * @see https://github.com/graphql/dataloader#caching-errors + */ + if (response instanceof Error) { + response = requestIDs.map((requestId) => { + /** + * Since we're returning an error object and not the + * expected return type from the resource, this element + * would be unsortable, since it wouldn't have the + * "reorderResultsByKey" attribute. + * + * Let's add it to the error object, as "reorderResultsByValue". + * + * (If we didn't specify that this resource needs + * sorting, then this will be "null" and won't be used.) + */ + const reorderResultsByValue = null; + + // Tell flow that "response" is actually an error object. + // (This is so we can pass it as 'cause' to CaughtResourceError) + invariant(response instanceof Error, 'expected response to be an error'); + + return new CaughtResourceError( + `[dataloader-codegen :: getFilmsV4] Caught error during call to resource. Error: ${response.stack}`, + response, + reorderResultsByValue, + ); + }); + } + + return response; + }), + ); + + /** + * When there's propertyBatchKey and propertyNewKey, the resource might + * contain less number of items that we requested. We need the value of batchKey and + * propertyBatchKey in requests group to help us split the results back up into the + * order that they were requested. + * + * + * Example: + * + * getBatchKeyForPartitionItems( + * 'bar_id', + * ['bar_id', 'property'], + * [ + * { bar_id: 2, property: 'name', include_extra_info: true }, + * { bar_id: 3, property: 'rating', include_extra_info: false }, + * { bar_id: 4, property: 'rating', include_extra_info: true }, + * ]) + * + * + * Returns: + * [ [ 2, 4 ], [ 3 ] ] + * + * getBatchKeyForPartitionItems( + * 'property', + * ['bar_id', 'property'], + * [ + * { bar_id: 2, property: 'name', include_extra_info: true }, + * { bar_id: 3, property: 'rating', include_extra_info: false }, + * { bar_id: 4, property: 'rating', include_extra_info: true }, + * ]) + * + * + * Returns: + * [ [ 'name', 'rating' ], [ 'rating' ] ] + */ + if (true && true) { + const batchKeyPartition = getBatchKeysForPartitionItems('film_id', ['film_id', 'property'], keys); + const propertyBatchKeyPartiion = getBatchKeysForPartitionItems( + 'property', + ['film_id', 'property'], + keys, + ); + + return unPartitionResultsByBatchKeyPartition( + 'film_id', + 'properties', + batchKeyPartition, + propertyBatchKeyPartiion, + requestGroups, + groupedResults, + ); + } else { + // Split the results back up into the order that they were requested + return unPartitionResults(requestGroups, groupedResults); + } + }, + { + ...cacheKeyOptions, + }, + ), getRoot: new DataLoader< $Call]>, $Call< diff --git a/examples/swapi/swapi-server.js b/examples/swapi/swapi-server.js index 43efb6a..1090b4c 100644 --- a/examples/swapi/swapi-server.js +++ b/examples/swapi/swapi-server.js @@ -20,16 +20,12 @@ const createSWAPIServer = () => { director: String } - type FilmV2 { - title: String - episodeNumber: Int - director: String - } - type Query { planet(id: Int): Planet film(id: Int): Film - filmv2(id: Int): FilmV2 + filmv2(id: Int): Film + filmv3(id: Int): Film + filmv4(id: Int): Film } `); @@ -165,6 +161,97 @@ const createSWAPIServer = () => { } } + class FilmModelV3 { + id: number; + + constructor(id: number) { + this.id = id; + } + + async title() { + const response = await swapiLoaders.getFilmsV3.load({ film_id: this.id, property: 'title' }); + + if (response instanceof Error) { + return response; + } + + if (response) { + return response.properties.title; + } + } + + async episodeNumber() { + const response = await swapiLoaders.getFilmsV3.load({ film_id: this.id, property: 'episode_id' }); + + if (response instanceof Error) { + return response; + } + + if (response) { + return response.properties.episode_id; + } + } + + async director() { + const response = await swapiLoaders.getFilmsV3.load({ film_id: this.id, property: 'director' }); + + if (response instanceof Error) { + return response; + } + + if (response) { + return response.properties.director; + } + } + } + + class FilmModelV4 { + id: number; + + constructor(id: number) { + this.id = id; + } + + async title() { + const response = await swapiLoaders.getFilmsV4.load({ film_id: this.id, property: 'title' }); + const stringId = this.id.toString(); + + if (response instanceof Error) { + return response; + } + + if (response) { + return response[stringId].title; + } + } + + async episodeNumber() { + const response = await swapiLoaders.getFilmsV4.load({ film_id: this.id, property: 'episode_id' }); + const stringId = this.id.toString(); + + if (response instanceof Error) { + return response; + } + + if (response) { + return response[stringId].episode_id; + } + } + + async director() { + const response = await swapiLoaders.getFilmsV4.load({ film_id: this.id, property: 'director' }); + const stringId = this.id.toString(); + + if (response instanceof Error) { + return response; + } + + if (response) { + return response[stringId].director; + } + } + } + const root = { planet: ({ id }) => { return new PlanetModel(id); @@ -175,6 +262,12 @@ const createSWAPIServer = () => { filmv2: ({ id }) => { return new FilmModelV2(id); }, + filmv3: ({ id }) => { + return new FilmModelV3(id); + }, + filmv4: ({ id }) => { + return new FilmModelV4(id); + }, }; return { schema, root }; @@ -215,6 +308,16 @@ runQuery(/* GraphQL */ ` episodeNumber director } + theBestV3: filmv3(id: 4) { + title + episodeNumber + director + } + theBestV4: filmv4(id: 4) { + title + episodeNumber + director + } } `).then((result) => { console.log(JSON.stringify(result, null, 4)); diff --git a/examples/swapi/swapi.dataloader-config.yaml b/examples/swapi/swapi.dataloader-config.yaml index e8376a1..9a7f136 100644 --- a/examples/swapi/swapi.dataloader-config.yaml +++ b/examples/swapi/swapi.dataloader-config.yaml @@ -35,6 +35,21 @@ resources: newKey: film_id propertyBatchKey: properties propertyNewKey: property + nestedPath: properties + getFilmsV3: + docsLink: https://swapi.dev/documentation#films + isBatchResource: true + batchKey: film_ids + newKey: film_id + propertyBatchKey: properties + propertyNewKey: property + getFilmsV4: + docsLink: https://swapi.dev/documentation#films + isBatchResource: true + batchKey: film_ids + newKey: film_id + propertyBatchKey: properties + propertyNewKey: property getRoot: docsLink: https://swapi.dev/documentation#root isBatchResource: false diff --git a/examples/swapi/swapi.js b/examples/swapi/swapi.js index cd3a6eb..f4b0aff 100644 --- a/examples/swapi/swapi.js +++ b/examples/swapi/swapi.js @@ -63,7 +63,7 @@ export type SWAPI_Film = $ReadOnly<{| edited: string, |}>; -export type SWAPI_Film_v2 = $ReadOnly<{| +export type SWAPI_Film_V2 = $ReadOnly<{| film_id: number, title: string, episode_id: number, @@ -71,6 +71,25 @@ export type SWAPI_Film_v2 = $ReadOnly<{| producer: string, |}>; +export type SWAPI_Film_V3 = $ReadOnly<{| + film_id: number, + properties: {| + title: string, + episode_id: number, + director: string, + producer: string, + |}, +|}>; + +export type SWAPI_Film_V4 = $ReadOnly<{| + number: {| + title: string, + episode_id: number, + director: string, + producer: string, + |}, +|}>; + export type SWAPI_Vehicle = $ReadOnly<{| name: string, key: string, @@ -93,7 +112,15 @@ export type SWAPIClientlibTypes = {| getFilmsV2: ({| film_ids: $ReadOnlyArray, properties: $ReadOnlyArray, - |}) => $ReadOnlyArray, + |}) => Promise<$ReadOnlyArray>, + getFilmsV3: ({| + film_ids: $ReadOnlyArray, + properties: $ReadOnlyArray, + |}) => Promise<$ReadOnlyArray>, + getFilmsV4: ({| + film_ids: $ReadOnlyArray, + properties: $ReadOnlyArray, + |}) => Promise<$ReadOnlyArray>, getRoot: ({||}) => Promise, |}; @@ -116,13 +143,40 @@ module.exports = function (): SWAPIClientlibTypes { [...film_ids].map((id) => fetch(url.resolve(SWAPI_URL, `films/${id}`)).then((res) => res.json())), ), getFilmsV2: ({ film_ids, properties }) => { + return { + properties: [ + { + film_id: 4, + director: 'George Lucas', + producer: 'Rick McCallum', + episode_id: 1, + title: 'The Phantom Menace', + }, + ], + }; + }, + getFilmsV3: ({ film_ids, properties }) => { return [ { film_id: 4, - director: 'George Lucas', - producer: 'Rick McCallum', - episode_id: 1, - title: 'The Phantom Menace', + properties: { + director: 'George Lucas', + producer: 'Rick McCallum', + episode_id: 1, + title: 'The Phantom Menace', + }, + }, + ]; + }, + getFilmsV4: ({ film_ids, properties }) => { + return [ + { + 4: { + director: 'George Lucas', + producer: 'Rick McCallum', + episode_id: 1, + title: 'The Phantom Menace', + }, }, ]; }, From 16d3180b3ed21b0fa06bcc225c67885a05e3c194 Mon Sep 17 00:00:00 2001 From: Yue Guo Date: Fri, 4 Jun 2021 14:47:09 -0700 Subject: [PATCH 20/24] add comments and change return value to Promise --- examples/swapi/swapi-server.js | 9 +++------ examples/swapi/swapi.js | 33 ++++++++++++++++++--------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/examples/swapi/swapi-server.js b/examples/swapi/swapi-server.js index 1090b4c..b7367ce 100644 --- a/examples/swapi/swapi-server.js +++ b/examples/swapi/swapi-server.js @@ -214,40 +214,37 @@ const createSWAPIServer = () => { async title() { const response = await swapiLoaders.getFilmsV4.load({ film_id: this.id, property: 'title' }); - const stringId = this.id.toString(); if (response instanceof Error) { return response; } if (response) { - return response[stringId].title; + return response[this.id].title; } } async episodeNumber() { const response = await swapiLoaders.getFilmsV4.load({ film_id: this.id, property: 'episode_id' }); - const stringId = this.id.toString(); if (response instanceof Error) { return response; } if (response) { - return response[stringId].episode_id; + return response[this.id].episode_id; } } async director() { const response = await swapiLoaders.getFilmsV4.load({ film_id: this.id, property: 'director' }); - const stringId = this.id.toString(); if (response instanceof Error) { return response; } if (response) { - return response[stringId].director; + return response[this.id].director; } } } diff --git a/examples/swapi/swapi.js b/examples/swapi/swapi.js index f4b0aff..f637f5c 100644 --- a/examples/swapi/swapi.js +++ b/examples/swapi/swapi.js @@ -64,11 +64,13 @@ export type SWAPI_Film = $ReadOnly<{| |}>; export type SWAPI_Film_V2 = $ReadOnly<{| - film_id: number, - title: string, - episode_id: number, - director: string, - producer: string, + properties: $ReadOnlyArray<{| + film_id: number, + title: string, + episode_id: number, + director: string, + producer: string, + |}>, |}>; export type SWAPI_Film_V3 = $ReadOnly<{| @@ -82,7 +84,7 @@ export type SWAPI_Film_V3 = $ReadOnly<{| |}>; export type SWAPI_Film_V4 = $ReadOnly<{| - number: {| + [number]: {| title: string, episode_id: number, director: string, @@ -109,10 +111,12 @@ export type SWAPIClientlibTypes = {| getPeople: ({| people_ids: $ReadOnlyArray |}) => Promise<$ReadOnlyArray>, getVehicles: ({| vehicle_ids: $ReadOnlyArray |}) => Promise<$ReadOnlyArray>, getFilms: ({| film_ids: Set |}) => Promise<$ReadOnlyArray>, + getRoot: ({||}) => Promise, + // create fake resource with different interfaces to test batch properties feature getFilmsV2: ({| film_ids: $ReadOnlyArray, properties: $ReadOnlyArray, - |}) => Promise<$ReadOnlyArray>, + |}) => Promise, getFilmsV3: ({| film_ids: $ReadOnlyArray, properties: $ReadOnlyArray, @@ -121,7 +125,6 @@ export type SWAPIClientlibTypes = {| film_ids: $ReadOnlyArray, properties: $ReadOnlyArray, |}) => Promise<$ReadOnlyArray>, - getRoot: ({||}) => Promise, |}; module.exports = function (): SWAPIClientlibTypes { @@ -142,8 +145,9 @@ module.exports = function (): SWAPIClientlibTypes { Promise.all( [...film_ids].map((id) => fetch(url.resolve(SWAPI_URL, `films/${id}`)).then((res) => res.json())), ), + getRoot: ({}) => fetch(SWAPI_URL).then((res) => res.json()), getFilmsV2: ({ film_ids, properties }) => { - return { + return Promise.resolve({ properties: [ { film_id: 4, @@ -153,10 +157,10 @@ module.exports = function (): SWAPIClientlibTypes { title: 'The Phantom Menace', }, ], - }; + }); }, getFilmsV3: ({ film_ids, properties }) => { - return [ + return Promise.resolve([ { film_id: 4, properties: { @@ -166,10 +170,10 @@ module.exports = function (): SWAPIClientlibTypes { title: 'The Phantom Menace', }, }, - ]; + ]); }, getFilmsV4: ({ film_ids, properties }) => { - return [ + return Promise.resolve([ { 4: { director: 'George Lucas', @@ -178,8 +182,7 @@ module.exports = function (): SWAPIClientlibTypes { title: 'The Phantom Menace', }, }, - ]; + ]); }, - getRoot: ({}) => fetch(SWAPI_URL).then((res) => res.json()), }; }; From 812c8466223cb8da78ce3efde38b137180611a60 Mon Sep 17 00:00:00 2001 From: Yue Guo Date: Fri, 4 Jun 2021 15:25:26 -0700 Subject: [PATCH 21/24] update API_DOCS.md --- API_DOCS.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/API_DOCS.md b/API_DOCS.md index d07d8af..b1a7b43 100644 --- a/API_DOCS.md +++ b/API_DOCS.md @@ -94,6 +94,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) + propertyNewKey: ?string (can only use if isBatchResource=true) typings: language: flow @@ -125,6 +127,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) (Optional) The argument to the resource that represents the nested list of optional properties we want to fetch. (e.g. usually 'properties' or 'features'). | +| `propertyNewKey` | (Optional) (Optional) The argument we'll replace the propertyBatchKey with - should be a singular version of the propertyBatchKey (e.g. usually 'property' or 'feature'). | ### `typings` From 92765fb0ae860c1211614a81875b595844e5ab76 Mon Sep 17 00:00:00 2001 From: Yue Guo Date: Fri, 11 Jun 2021 14:36:20 -0700 Subject: [PATCH 22/24] add enum mergePropertyOptions to control what style of endpoint it is --- __tests__/implementation.test.js | 12 ++++++ src/config.ts | 10 +++++ src/implementation.ts | 4 +- src/runtimeHelpers.ts | 73 +++++++++++++++++--------------- 4 files changed, 65 insertions(+), 34 deletions(-) diff --git a/__tests__/implementation.test.js b/__tests__/implementation.test.js index fe6f198..fdecf73 100644 --- a/__tests__/implementation.test.js +++ b/__tests__/implementation.test.js @@ -1227,6 +1227,7 @@ test('batch endpoint (multiple requests) with propertyBatchKey', async () => { newKey: 'foo_id', propertyBatchKey: 'properties', propertyNewKey: 'property', + mergePropertyConfig: 'PropertyAtTopLevel', }, }, }; @@ -1282,6 +1283,7 @@ test('batch endpoint (multiple requests) with propertyBatchKey returned in a nes newKey: 'foo_id', propertyBatchKey: 'properties', propertyNewKey: 'property', + mergePropertyConfig: 'PropertyInNestedObject', }, }, }; @@ -1339,6 +1341,7 @@ test('batch endpoint (with commaSeparatedBatchKey) with propertyBatchKey', async commaSeparatedBatchKey: true, propertyBatchKey: 'properties', propertyNewKey: 'property', + mergePropertyConfig: 'PropertyInNestedObject', }, }, }; @@ -1381,6 +1384,7 @@ test('batch endpoint (with nestedPath) with propertyBatchKey', async () => { nestedPath: 'foos', propertyBatchKey: 'properties', propertyNewKey: 'property', + mergePropertyConfig: 'PropertyInNestedObject', }, }, }; @@ -1424,6 +1428,7 @@ test('batch endpoint (multiple requests) with propertyBatchKey that rejects', as newKey: 'foo_id', propertyBatchKey: 'properties', propertyNewKey: 'property', + mergePropertyConfig: 'PropertyAtTopLevel', }, }, }; @@ -1489,6 +1494,7 @@ test('batch endpoint (multiple requests) with propertyBatchKey error handling', newKey: 'foo_id', propertyBatchKey: 'properties', propertyNewKey: 'property', + mergePropertyConfig: 'PropertyAtTopLevel', }, }, }; @@ -1554,6 +1560,7 @@ test('batch endpoint with propertyBatchKey without reorderResultsByKey throws er newKey: 'foo_id', propertyBatchKey: 'properties', propertyNewKey: 'property', + mergePropertyConfig: 'PropertyAtTopLevel', }, }, }; @@ -1619,6 +1626,7 @@ test('batch endpoint with propertyBatchKey with isResponseDictionary with a miss isResponseDictionary: true, propertyBatchKey: 'properties', propertyNewKey: 'property', + mergePropertyConfig: 'PropertyInNestedObject', }, }, }; @@ -1662,6 +1670,7 @@ test('batch endpoint (multiple requests) with propertyBatchKey different respons newKey: 'foo_id', propertyBatchKey: 'properties', propertyNewKey: 'property', + mergePropertyConfig: 'IdPropertyPair', }, }, }; @@ -1714,6 +1723,7 @@ test('batch endpoint with propertyBatchKey with reorderResultsByKey handles resp reorderResultsByKey: 'foo_id', propertyBatchKey: 'properties', propertyNewKey: 'property', + mergePropertyConfig: 'PropertyInNestedObject', }, }, }; @@ -1764,6 +1774,7 @@ test('batch endpoint with propertyBatchKey (multiple requests, error handling - newKey: 'foo_id', propertyBatchKey: 'properties', propertyNewKey: 'property', + mergePropertyConfig: 'PropertyAtTopLevel', }, }, }; @@ -1817,6 +1828,7 @@ test('batch endpoint with propertyBatchKey (multiple requests, error handling, w reorderResultsByKey: 'foo_id', propertyBatchKey: 'properties', propertyNewKey: 'property', + mergePropertyConfig: 'PropertyInNestedObject', }, }, }; diff --git a/src/config.ts b/src/config.ts index f47e645..864697b 100644 --- a/src/config.ts +++ b/src/config.ts @@ -16,12 +16,22 @@ export interface GlobalConfig { resources: any; } +export enum mergePropertyOptions { + // case 1: properties are returned in a nested object, id is at the top level. + PropertyInNestedObject = 'PropertyInNestedObject', + // case 2: properties are not returned in a nested object, but spread at top level as well. + PropertyAtTopLevel = 'PropertyAtTopLevel', + // case 3: id and its properties are returned as a key-value pair at the top level. + IdPropertyPair = 'IdPropertyPair', +} + export interface BatchResourceConfig { isBatchResource: true; batchKey: string; newKey: string; propertyBatchKey: string; propertyNewKey: string; + mergePropertyConfig: mergePropertyOptions; reorderResultsByKey?: string; nestedPath?: string; commaSeparatedBatchKey?: boolean; diff --git a/src/implementation.ts b/src/implementation.ts index fc19980..d76d3e4 100644 --- a/src/implementation.ts +++ b/src/implementation.ts @@ -480,7 +480,8 @@ function getBatchLoader(resourceConfig: BatchResourceConfig, resourcePath: Reado */ if ( ${typeof resourceConfig.propertyNewKey === 'string'} && - ${typeof resourceConfig.propertyBatchKey === 'string'}) { + ${typeof resourceConfig.propertyBatchKey === 'string'} && + ${typeof resourceConfig.mergePropertyConfig === 'string'}) { const batchKeyPartition = getBatchKeysForPartitionItems( '${resourceConfig.newKey}', ['${resourceConfig.newKey}', '${resourceConfig.propertyNewKey}'], @@ -495,6 +496,7 @@ function getBatchLoader(resourceConfig: BatchResourceConfig, resourcePath: Reado return unPartitionResultsByBatchKeyPartition( '${resourceConfig.newKey}', '${resourceConfig.propertyBatchKey}', + '${resourceConfig.mergePropertyConfig}', batchKeyPartition, propertyBatchKeyPartiion, requestGroups, diff --git a/src/runtimeHelpers.ts b/src/runtimeHelpers.ts index c56bf11..b653a2e 100644 --- a/src/runtimeHelpers.ts +++ b/src/runtimeHelpers.ts @@ -8,6 +8,7 @@ import AggregateError from 'aggregate-error'; import ensureError from 'ensure-error'; import invariant from 'assert'; import objectHash from 'object-hash'; +import { mergePropertyOptions } from './config'; export function errorPrefix(resourcePath: ReadonlyArray): string { return `[dataloader-codegen :: ${resourcePath.join('.')}]`; @@ -362,6 +363,7 @@ export function unPartitionResults( export function unPartitionResultsByBatchKeyPartition>( newKey: string, propertyBatchKey: string, + mergePropertyConfig: mergePropertyOptions, batchKeyPartition: ReadonlyArray>, propertyBatchKeyPartion: ReadonlyArray>, /** Should be a nested array of IDs, as generated by partitionItems */ @@ -393,15 +395,16 @@ export function unPartitionResultsByBatchKeyPartition ({ [key]: resultElement[key], })), ); + break; + } + } else if (mergePropertyConfig == 'IdPropertyPair') { + /** + * Case 3: newKey and its properties are returned as a key-value pair at the top level. + * e.g.: { 2: { name: 'Burger King', rating: 3 } } + * If requested property exist in resultElement, we restructure data, so that we only return + * the property we asked for. + */ + if ( + resultElement.hasOwnProperty(batchKeyPartition[i][j]) && + resultElement[batchKeyPartition[i][j]].hasOwnProperty(propertyBatchKeyPartion[i][j]) + ) { + result = { + [batchKeyPartition[i][j]]: { + [propertyBatchKeyPartion[i][j]]: + resultElement[batchKeyPartition[i][j]][propertyBatchKeyPartion[i][j]], + }, + }; + break; } - break; - } - /** - * Case 3: newKey and its properties are returned as a key-value pair at the top level. - * e.g.: { 2: { name: 'Burger King', rating: 3 } } - * If requested property exist in resultElement, we restructure data, so that we only return - * the property we asked for. - */ - if ( - resultElement.hasOwnProperty(batchKeyPartition[i][j]) && - resultElement[batchKeyPartition[i][j]].hasOwnProperty(propertyBatchKeyPartion[i][j]) - ) { - result = { - [batchKeyPartition[i][j]]: { - [propertyBatchKeyPartion[i][j]]: - resultElement[batchKeyPartition[i][j]][propertyBatchKeyPartion[i][j]], - }, - }; - break; } } From 7b08aa2d16cfb481619318699d07b284f1f92f32 Mon Sep 17 00:00:00 2001 From: Yue Guo Date: Fri, 11 Jun 2021 14:59:29 -0700 Subject: [PATCH 23/24] update swapi examples --- examples/swapi/swapi-loaders.js | 30 ++++++++++++++------- examples/swapi/swapi.dataloader-config.yaml | 3 +++ schema.json | 4 +++ 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/examples/swapi/swapi-loaders.js b/examples/swapi/swapi-loaders.js index 6f2c6a6..4623e4d 100644 --- a/examples/swapi/swapi-loaders.js +++ b/examples/swapi/swapi-loaders.js @@ -623,7 +623,7 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade * Returns: * [ [ 'name', 'rating' ], [ 'rating' ] ] */ - if (false && false) { + if (false && false && false) { const batchKeyPartition = getBatchKeysForPartitionItems( 'planet_id', ['planet_id', 'undefined'], @@ -638,6 +638,7 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade return unPartitionResultsByBatchKeyPartition( 'planet_id', 'undefined', + 'undefined', batchKeyPartition, propertyBatchKeyPartiion, requestGroups, @@ -970,7 +971,7 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade * Returns: * [ [ 'name', 'rating' ], [ 'rating' ] ] */ - if (false && false) { + if (false && false && false) { const batchKeyPartition = getBatchKeysForPartitionItems( 'person_id', ['person_id', 'undefined'], @@ -985,6 +986,7 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade return unPartitionResultsByBatchKeyPartition( 'person_id', 'undefined', + 'undefined', batchKeyPartition, propertyBatchKeyPartiion, requestGroups, @@ -1320,7 +1322,7 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade * Returns: * [ [ 'name', 'rating' ], [ 'rating' ] ] */ - if (false && false) { + if (false && false && false) { const batchKeyPartition = getBatchKeysForPartitionItems( 'vehicle_id', ['vehicle_id', 'undefined'], @@ -1335,6 +1337,7 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade return unPartitionResultsByBatchKeyPartition( 'vehicle_id', 'undefined', + 'undefined', batchKeyPartition, propertyBatchKeyPartiion, requestGroups, @@ -1674,7 +1677,7 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade * Returns: * [ [ 'name', 'rating' ], [ 'rating' ] ] */ - if (false && false) { + if (false && false && false) { const batchKeyPartition = getBatchKeysForPartitionItems('film_id', ['film_id', 'undefined'], keys); const propertyBatchKeyPartiion = getBatchKeysForPartitionItems( 'undefined', @@ -1685,6 +1688,7 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade return unPartitionResultsByBatchKeyPartition( 'film_id', 'undefined', + 'undefined', batchKeyPartition, propertyBatchKeyPartiion, requestGroups, @@ -1753,7 +1757,8 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade * "newKey": "film_id", * "propertyBatchKey": "properties", * "propertyNewKey": "property", - * "nestedPath": "properties" + * "nestedPath": "properties", + * "mergePropertyConfig": "PropertyAtTopLevel" * } * ``` */ @@ -2043,7 +2048,7 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade * Returns: * [ [ 'name', 'rating' ], [ 'rating' ] ] */ - if (true && true) { + if (true && true && true) { const batchKeyPartition = getBatchKeysForPartitionItems('film_id', ['film_id', 'property'], keys); const propertyBatchKeyPartiion = getBatchKeysForPartitionItems( 'property', @@ -2054,6 +2059,7 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade return unPartitionResultsByBatchKeyPartition( 'film_id', 'properties', + 'PropertyAtTopLevel', batchKeyPartition, propertyBatchKeyPartiion, requestGroups, @@ -2118,7 +2124,8 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade * "batchKey": "film_ids", * "newKey": "film_id", * "propertyBatchKey": "properties", - * "propertyNewKey": "property" + * "propertyNewKey": "property", + * "mergePropertyConfig": "PropertyInNestedObject" * } * ``` */ @@ -2374,7 +2381,7 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade * Returns: * [ [ 'name', 'rating' ], [ 'rating' ] ] */ - if (true && true) { + if (true && true && true) { const batchKeyPartition = getBatchKeysForPartitionItems('film_id', ['film_id', 'property'], keys); const propertyBatchKeyPartiion = getBatchKeysForPartitionItems( 'property', @@ -2385,6 +2392,7 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade return unPartitionResultsByBatchKeyPartition( 'film_id', 'properties', + 'PropertyInNestedObject', batchKeyPartition, propertyBatchKeyPartiion, requestGroups, @@ -2449,7 +2457,8 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade * "batchKey": "film_ids", * "newKey": "film_id", * "propertyBatchKey": "properties", - * "propertyNewKey": "property" + * "propertyNewKey": "property", + * "mergePropertyConfig": "IdPropertyPair" * } * ``` */ @@ -2705,7 +2714,7 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade * Returns: * [ [ 'name', 'rating' ], [ 'rating' ] ] */ - if (true && true) { + if (true && true && true) { const batchKeyPartition = getBatchKeysForPartitionItems('film_id', ['film_id', 'property'], keys); const propertyBatchKeyPartiion = getBatchKeysForPartitionItems( 'property', @@ -2716,6 +2725,7 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade return unPartitionResultsByBatchKeyPartition( 'film_id', 'properties', + 'IdPropertyPair', batchKeyPartition, propertyBatchKeyPartiion, requestGroups, diff --git a/examples/swapi/swapi.dataloader-config.yaml b/examples/swapi/swapi.dataloader-config.yaml index 9a7f136..cef00cd 100644 --- a/examples/swapi/swapi.dataloader-config.yaml +++ b/examples/swapi/swapi.dataloader-config.yaml @@ -36,6 +36,7 @@ resources: propertyBatchKey: properties propertyNewKey: property nestedPath: properties + mergePropertyConfig: PropertyAtTopLevel getFilmsV3: docsLink: https://swapi.dev/documentation#films isBatchResource: true @@ -43,6 +44,7 @@ resources: newKey: film_id propertyBatchKey: properties propertyNewKey: property + mergePropertyConfig: PropertyInNestedObject getFilmsV4: docsLink: https://swapi.dev/documentation#films isBatchResource: true @@ -50,6 +52,7 @@ resources: newKey: film_id propertyBatchKey: properties propertyNewKey: property + mergePropertyConfig: IdPropertyPair getRoot: docsLink: https://swapi.dev/documentation#root isBatchResource: false diff --git a/schema.json b/schema.json index 99f0f10..bf3fffc 100644 --- a/schema.json +++ b/schema.json @@ -105,6 +105,10 @@ "propertyNewKey": { "type": "string", "description": "(Optional) The argument we'll replace the propertyBatchKey with - should be a singular version of the propertyBatchKey (e.g. usually 'property' or 'feature')" + }, + "mergePropertyConfig": { + "type": "string", + "description": "(Optional) A string enum to control what style of the resource it is. Currently, 'PropertyInNestedObject', 'PropertyAtTopLevel' and 'IdPropertyPair' are the only 3 supported styles" } } }, From c25816bd611ba52ad34fc9fc8a1e1717c3939a8b Mon Sep 17 00:00:00 2001 From: Yue Guo Date: Wed, 30 Jun 2021 16:19:45 -0700 Subject: [PATCH 24/24] merge with master --- examples/swapi/swapi-loaders.js | 328 +------------------- examples/swapi/swapi.dataloader-config.yaml | 1 - 2 files changed, 8 insertions(+), 321 deletions(-) diff --git a/examples/swapi/swapi-loaders.js b/examples/swapi/swapi-loaders.js index 2a47fd0..2e141dc 100644 --- a/examples/swapi/swapi-loaders.js +++ b/examples/swapi/swapi-loaders.js @@ -204,15 +204,15 @@ export type LoadersType = $ReadOnly<{| >, |}, |}, - $PropertyType< - $ElementType< + $ElementType< + $PropertyType< $Call< ExtractPromisedReturnValue<[$Call]>]>, $PropertyType, >, - 0, + 'properties', >, - 'properties', + 0, >, // This third argument is the cache key type. Since we use objectHash in cacheKeyOptions, this is "string". string, @@ -1729,15 +1729,15 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade >, |}, |}, - $PropertyType< - $ElementType< + $ElementType< + $PropertyType< $Call< ExtractPromisedReturnValue<[$Call]>]>, $PropertyType, >, - 0, + 'properties', >, - 'properties', + 0, >, // This third argument is the cache key type. Since we use objectHash in cacheKeyOptions, this is "string". string, @@ -2740,318 +2740,6 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade ...cacheKeyOptions, }, ), - getFilmsV2: new DataLoader< - {| - ...$Diff< - $Call]>, - { - film_ids: $PropertyType< - $Call]>, - 'film_ids', - >, - }, - >, - ...{| - film_id: $ElementType< - $PropertyType<$Call]>, 'film_ids'>, - 0, - >, - |}, - |}, - $ElementType< - $PropertyType< - $Call< - ExtractPromisedReturnValue<[$Call]>]>, - $PropertyType, - >, - 'properties', - >, - 0, - >, - // This third argument is the cache key type. Since we use objectHash in cacheKeyOptions, this is "string". - string, - >( - /** - * =============================================================== - * Generated DataLoader: getFilmsV2 - * =============================================================== - * - * Resource Config: - * - * ```json - * { - * "docsLink": "https://swapi.dev/documentation#films", - * "isBatchResource": true, - * "batchKey": "film_ids", - * "newKey": "film_id", - * "nestedPath": "properties" - * } - * ``` - */ - async (keys) => { - invariant( - typeof resources.getFilmsV2 === 'function', - [ - '[dataloader-codegen :: getFilmsV2] resources.getFilmsV2 is not a function.', - 'Did you pass in an instance of getFilmsV2 to "getLoaders"?', - ].join(' '), - ); - - /** - * Chunk up the "keys" array to create a set of "request groups". - * - * We're about to hit a batch resource. In addition to the batch - * key, the resource may take other arguments too. When batching - * up requests, we'll want to look out for where those other - * arguments differ, and send multiple requests so we don't get - * back the wrong info. - * - * In other words, we'll potentially want to send _multiple_ - * requests to the underlying resource batch method in this - * dataloader body. - * - * ~~~ Why? ~~~ - * - * Consider what happens when we get called with arguments where - * the non-batch keys differ. - * - * Example: - * - * ```js - * loaders.foo.load({ foo_id: 2, include_private_data: true }); - * loaders.foo.load({ foo_id: 3, include_private_data: false }); - * loaders.foo.load({ foo_id: 4, include_private_data: false }); - * ``` - * - * If we collected everything up and tried to send the one - * request to the resource as a batch request, how do we know - * what the value for "include_private_data" should be? We're - * going to have to group these up up and send two requests to - * the resource to make sure we're requesting the right stuff. - * - * e.g. We'd need to make the following set of underlying resource - * calls: - * - * ```js - * foo({ foo_ids: [ 2 ], include_private_data: true }); - * foo({ foo_ids: [ 3, 4 ], include_private_data: false }); - * ``` - * - * ~~~ tl;dr ~~~ - * - * When we have calls to .load with differing non batch key args, - * we'll need to send multiple requests to the underlying - * resource to make sure we get the right results back. - * - * Let's create the request groups, where each element in the - * group refers to a position in "keys" (i.e. a call to .load) - * - * Example: - * - * ```js - * partitionItems([ - * { bar_id: 7, include_extra_info: true }, - * { bar_id: 8, include_extra_info: false }, - * { bar_id: 9, include_extra_info: true }, - * ], 'bar_id') - * ``` - * - * Returns: - * `[ [ 0, 2 ], [ 1 ] ]` - * - * We'll refer to each element in the group as a "request ID". - */ - const requestGroups = partitionItems('film_id', keys); - - // Map the request groups to a list of Promises - one for each request - const groupedResults = await Promise.all( - requestGroups.map(async (requestIDs) => { - /** - * Select a set of elements in "keys", where all non-batch - * keys should be identical. - * - * We're going to smoosh all these together into one payload to - * send to the resource as a batch request! - */ - const requests = requestIDs.map((id) => keys[id]); - - // For now, we assume that the dataloader key should be the first argument to the resource - // @see https://github.com/Yelp/dataloader-codegen/issues/56 - const resourceArgs = [ - { - ..._.omit(requests[0], 'film_id'), - ['film_ids']: requests.map((k) => k['film_id']), - }, - ]; - - let response = await (async (_resourceArgs) => { - // Make a re-assignable variable so flow/eslint doesn't complain - let __resourceArgs = _resourceArgs; - - if (options && options.resourceMiddleware && options.resourceMiddleware.before) { - __resourceArgs = await options.resourceMiddleware.before( - ['getFilmsV2'], - __resourceArgs, - ); - } - - let _response; - try { - // Finally, call the resource! - _response = await resources.getFilmsV2(...__resourceArgs); - } catch (error) { - const errorHandler = - options && typeof options.errorHandler === 'function' - ? options.errorHandler - : defaultErrorHandler; - - /** - * Apply some error handling to catch and handle all errors/rejected promises. errorHandler must return an Error object. - * - * If we let errors here go unhandled here, it will bubble up and DataLoader will return an error for all - * keys requested. We can do slightly better by returning the error object for just the keys in this batch request. - */ - _response = await errorHandler(['getFilmsV2'], error); - - // Check that errorHandler actually returned an Error object, and turn it into one if not. - if (!(_response instanceof Error)) { - _response = new Error( - [ - `[dataloader-codegen :: getFilmsV2] Caught an error, but errorHandler did not return an Error object.`, - `Instead, got ${typeof _response}: ${util.inspect(_response)}`, - ].join(' '), - ); - } - } - - if (options && options.resourceMiddleware && options.resourceMiddleware.after) { - _response = await options.resourceMiddleware.after(['getFilmsV2'], _response); - } - - return _response; - })(resourceArgs); - - if (!(response instanceof Error)) { - /** - * Un-nest the actual data from the resource return value. - * - * e.g. - * ```js - * { - * foos: [ - * { id: 1, value: 'hello' }, - * { id: 2, value: 'world' }, - * ] - * } - * ``` - * - * Becomes - * - * ```js - * [ - * { id: 1, value: 'hello' }, - * { id: 2, value: 'world' }, - * ] - * ``` - */ - response = _.get( - response, - 'properties', - new Error( - [ - '[dataloader-codegen :: getFilmsV2]', - 'Tried to un-nest the response from the resource, but', - ".get(response, 'properties')", - 'was empty!', - ].join(' '), - ), - ); - } - - if (!(response instanceof Error)) { - if (!Array.isArray(response)) { - response = new Error( - ['[dataloader-codegen :: getFilmsV2]', 'Expected response to be an array!'].join( - ' ', - ), - ); - } - } - - if (!(response instanceof Error)) { - /** - * Check to see the resource contains the same number - * of items that we requested. If not, since there's - * no "reorderResultsByKey" specified for this resource, - * we don't know _which_ key's response is missing. Therefore - * it's unsafe to return the response array back. - */ - if (response.length !== requests.length) { - /** - * We must return errors for all keys in this group :( - */ - response = new BatchItemNotFoundError( - [ - `[dataloader-codegen :: getFilmsV2] Resource returned ${response.length} items, but we requested ${requests.length} items.`, - 'Add reorderResultsByKey to the config for this resource to be able to handle a partial response.', - ].join(' '), - ); - - // Tell flow that BatchItemNotFoundError extends Error. - // It's an issue with flowgen package, but not an issue with Flow. - // @see https://github.com/Yelp/dataloader-codegen/pull/35#discussion_r394777533 - invariant(response instanceof Error, 'expected BatchItemNotFoundError to be an Error'); - } - } - - /** - * If the resource returns an Error, we'll want to copy and - * return that error as the return value for every request in - * this group. - * - * This allow the error to be cached, and allows the rest of the - * requests made by this DataLoader to succeed. - * - * @see https://github.com/graphql/dataloader#caching-errors - */ - if (response instanceof Error) { - response = requestIDs.map((requestId) => { - /** - * Since we're returning an error object and not the - * expected return type from the resource, this element - * would be unsortable, since it wouldn't have the - * "reorderResultsByKey" attribute. - * - * Let's add it to the error object, as "reorderResultsByValue". - * - * (If we didn't specify that this resource needs - * sorting, then this will be "null" and won't be used.) - */ - const reorderResultsByValue = null; - - // Tell flow that "response" is actually an error object. - // (This is so we can pass it as 'cause' to CaughtResourceError) - invariant(response instanceof Error, 'expected response to be an error'); - - return new CaughtResourceError( - `[dataloader-codegen :: getFilmsV2] Caught error during call to resource. Error: ${response.stack}`, - response, - reorderResultsByValue, - ); - }); - } - - return response; - }), - ); - - // Split the results back up into the order that they were requested - return unPartitionResults(requestGroups, groupedResults); - }, - { - ...cacheKeyOptions, - }, - ), getRoot: new DataLoader< $Call]>, $Call< diff --git a/examples/swapi/swapi.dataloader-config.yaml b/examples/swapi/swapi.dataloader-config.yaml index 4da8a24..cef00cd 100644 --- a/examples/swapi/swapi.dataloader-config.yaml +++ b/examples/swapi/swapi.dataloader-config.yaml @@ -53,7 +53,6 @@ resources: propertyBatchKey: properties propertyNewKey: property mergePropertyConfig: IdPropertyPair - nestedPath: properties getRoot: docsLink: https://swapi.dev/documentation#root isBatchResource: false