diff --git a/README.md b/README.md index e06ee3c..9496a31 100644 --- a/README.md +++ b/README.md @@ -58,12 +58,12 @@ and can make a more efficient set of HTTP requests to the underlying resource. ```yaml resources: getPeople: - docsLink: https://swapi.co/documentation#people + docsLink: https://swapi.dev/documentation#people isBatchResource: true batchKey: people_ids newKey: person_id getPlanets: - docsLink: https://swapi.co/documentation#planets + docsLink: https://swapi.dev/documentation#planets isBatchResource: true batchKey: planet_ids newKey: planet_id @@ -85,7 +85,7 @@ and can make a more efficient set of HTTP requests to the underlying resource. ```js import getLoaders from './__codegen__/swapi-loaders'; - // StarWarsAPI is a clientlib containing fetch calls to swapi.co + // StarWarsAPI is a clientlib containing fetch calls to swapi.dev // getLoaders is the function that dataloader-codegen generates for us const swapiLoaders = getLoaders(StarWarsAPI); diff --git a/__tests__/implementation.test.js b/__tests__/implementation.test.js index 9a37ac3..210b006 100644 --- a/__tests__/implementation.test.js +++ b/__tests__/implementation.test.js @@ -939,7 +939,7 @@ test('middleware can transform the request args and the resource response', asyn }); }); -test('returning custom errors from error handler is supported', async () => { +test('[isBatchResource: true] returning custom errors from error handler is supported', async () => { class MyCustomError extends Error { constructor(...args) { super(...args); @@ -1020,6 +1020,73 @@ test('returning custom errors from error handler is supported', async () => { }); }); +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) { diff --git a/examples/swapi/Makefile b/examples/swapi/Makefile index 9a93c7b..6a0d020 100644 --- a/examples/swapi/Makefile +++ b/examples/swapi/Makefile @@ -8,5 +8,6 @@ swapi-loaders.js: flow-typed: node_modules yarn flow-typed install +.PHONY: build build: node_modules yarn babel *.js -d build diff --git a/examples/swapi/README.md b/examples/swapi/README.md index 3a23146..84edab2 100644 --- a/examples/swapi/README.md +++ b/examples/swapi/README.md @@ -1,6 +1,6 @@ # dataloader-codegen Example: Star Wars API (SWAPI) -Shows an example of a GraphQL Server using dataloader-codegen. Prints data from https://swapi.co/. +Shows an example of a GraphQL Server using dataloader-codegen. Prints data from https://swapi.dev. ## Try it out locally! @@ -28,5 +28,5 @@ $ node build/swapi-server.js ## File Layout: - `swapi-loaders.js`: An autogenerated file by dataloader-codegen. Contains the codegen'd dataloaders. (Checked in to git so folks can easily see an example of the generated code). -- `swapi.js`: A set of functions to fetch data from https://swapi.co/. This is analogous to a library generated by openapi-generator/swagger-codegen. +- `swapi.js`: A set of functions to fetch data from https://swapi.dev/. This is analogous to a library generated by openapi-generator/swagger-codegen. - `swapi-server.js`: The dummy GraphQL server! This imports the dataloaders from swapi-loaders.js. At present, it just prints the result of a query to stdout. diff --git a/examples/swapi/package.json b/examples/swapi/package.json index ac0f4a2..15dedda 100644 --- a/examples/swapi/package.json +++ b/examples/swapi/package.json @@ -3,12 +3,15 @@ "@babel/cli": "^7.8.4", "@babel/node": "^7.7.0", "@babel/preset-flow": "^7.0.0", - "flow-bin": "0.122.0", + "flow-bin": "0.123.0", "flow-typed": "^2.6.2" }, "dependencies": { "dataloader": "^2.0.0", "graphql": "15.0.0", "node-fetch": "^2.6.0" + }, + "engines": { + "node": ">=10" } } diff --git a/examples/swapi/swapi-loaders.js b/examples/swapi/swapi-loaders.js index 214c20a..d08b796 100644 --- a/examples/swapi/swapi-loaders.js +++ b/examples/swapi/swapi-loaders.js @@ -87,6 +87,8 @@ export type LoadersType = $ReadOnly<{| >, 0, >, + // This third argument is the cache key type. Since we use objectHash in cacheKeyOptions, this is "string". + string, >, getPeople: DataLoader< {| @@ -113,6 +115,8 @@ export type LoadersType = $ReadOnly<{| >, 0, >, + // This third argument is the cache key type. Since we use objectHash in cacheKeyOptions, this is "string". + string, >, getVehicles: DataLoader< {| @@ -139,6 +143,8 @@ export type LoadersType = $ReadOnly<{| >, 0, >, + // This third argument is the cache key type. Since we use objectHash in cacheKeyOptions, this is "string". + string, >, getRoot: DataLoader< $Call]>, @@ -146,6 +152,8 @@ export type LoadersType = $ReadOnly<{| ExtractPromisedReturnValue<[$Call]>]>, $PropertyType, >, + // This third argument is the cache key type. Since we use objectHash in cacheKeyOptions, this is "string". + string, >, |}>; @@ -176,6 +184,8 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade >, 0, >, + // This third argument is the cache key type. Since we use objectHash in cacheKeyOptions, this is "string". + string, >( /** * =============================================================== @@ -186,7 +196,7 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade * * ```json * { - * "docsLink": "https://swapi.co/documentation#planets", + * "docsLink": "https://swapi.dev/documentation#planets", * "isBatchResource": true, * "batchKey": "planet_ids", * "newKey": "planet_id" @@ -194,14 +204,13 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade * ``` */ async keys => { - if (typeof resources.getPlanets !== 'function') { - return Promise.reject( - [ - '[dataloader-codegen :: getPlanets] resources.getPlanets is not a function.', - 'Did you pass in an instance of getPlanets to "getLoaders"?', - ].join(' '), - ); - } + invariant( + typeof resources.getPlanets === 'function', + [ + '[dataloader-codegen :: getPlanets] resources.getPlanets is not a function.', + 'Did you pass in an instance of getPlanets to "getLoaders"?', + ].join(' '), + ); /** * Chunk up the "keys" array to create a set of "request groups". @@ -281,49 +290,61 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade */ const requests = requestIDs.map(id => keys[id]); - let resourceArgs = [ + // 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], 'planet_id'), ['planet_ids']: requests.map(k => k['planet_id']), }, ]; - if (options && options.resourceMiddleware && options.resourceMiddleware.before) { - resourceArgs = await options.resourceMiddleware.before(['getPlanets'], resourceArgs); - } + let response = await (async _resourceArgs => { + // Make a re-assignable variable so flow/eslint doesn't complain + let __resourceArgs = _resourceArgs; - let response; - try { - // Finally, call the resource! - response = await resources.getPlanets(...resourceArgs); - } catch (error) { - const errorHandler = - options && typeof options.errorHandler === 'function' - ? options.errorHandler - : defaultErrorHandler; + if (options && options.resourceMiddleware && options.resourceMiddleware.before) { + __resourceArgs = await options.resourceMiddleware.before( + ['getPlanets'], + __resourceArgs, + ); + } - /** - * 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(['getPlanets'], error); + let _response; + try { + // Finally, call the resource! + _response = await resources.getPlanets(...__resourceArgs); + } catch (error) { + const errorHandler = + options && typeof options.errorHandler === 'function' + ? options.errorHandler + : defaultErrorHandler; - // Check that errorHandler actually returned an Error object, and turn it into one if not. - if (!(response instanceof Error)) { - response = new Error( - [ - `[dataloader-codegen :: getPlanets] Caught an error, but errorHandler did not return an Error object.`, - `Instead, got ${typeof response}: ${util.inspect(response)}`, - ].join(' '), - ); + /** + * 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(['getPlanets'], 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 :: getPlanets] 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(['getPlanets'], response); - } + if (options && options.resourceMiddleware && options.resourceMiddleware.after) { + _response = await options.resourceMiddleware.after(['getPlanets'], _response); + } + + return _response; + })(resourceArgs); if (!(response instanceof Error)) { } @@ -437,6 +458,8 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade >, 0, >, + // This third argument is the cache key type. Since we use objectHash in cacheKeyOptions, this is "string". + string, >( /** * =============================================================== @@ -447,7 +470,7 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade * * ```json * { - * "docsLink": "https://swapi.co/documentation#people", + * "docsLink": "https://swapi.dev/documentation#people", * "isBatchResource": true, * "batchKey": "people_ids", * "newKey": "person_id" @@ -455,14 +478,13 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade * ``` */ async keys => { - if (typeof resources.getPeople !== 'function') { - return Promise.reject( - [ - '[dataloader-codegen :: getPeople] resources.getPeople is not a function.', - 'Did you pass in an instance of getPeople to "getLoaders"?', - ].join(' '), - ); - } + invariant( + typeof resources.getPeople === 'function', + [ + '[dataloader-codegen :: getPeople] resources.getPeople is not a function.', + 'Did you pass in an instance of getPeople to "getLoaders"?', + ].join(' '), + ); /** * Chunk up the "keys" array to create a set of "request groups". @@ -542,49 +564,58 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade */ const requests = requestIDs.map(id => keys[id]); - let resourceArgs = [ + // 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], 'person_id'), ['people_ids']: requests.map(k => k['person_id']), }, ]; - if (options && options.resourceMiddleware && options.resourceMiddleware.before) { - resourceArgs = await options.resourceMiddleware.before(['getPeople'], resourceArgs); - } + let response = await (async _resourceArgs => { + // Make a re-assignable variable so flow/eslint doesn't complain + let __resourceArgs = _resourceArgs; - let response; - try { - // Finally, call the resource! - response = await resources.getPeople(...resourceArgs); - } catch (error) { - const errorHandler = - options && typeof options.errorHandler === 'function' - ? options.errorHandler - : defaultErrorHandler; + if (options && options.resourceMiddleware && options.resourceMiddleware.before) { + __resourceArgs = await options.resourceMiddleware.before(['getPeople'], __resourceArgs); + } - /** - * 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(['getPeople'], error); + let _response; + try { + // Finally, call the resource! + _response = await resources.getPeople(...__resourceArgs); + } catch (error) { + const errorHandler = + options && typeof options.errorHandler === 'function' + ? options.errorHandler + : defaultErrorHandler; - // Check that errorHandler actually returned an Error object, and turn it into one if not. - if (!(response instanceof Error)) { - response = new Error( - [ - `[dataloader-codegen :: getPeople] Caught an error, but errorHandler did not return an Error object.`, - `Instead, got ${typeof response}: ${util.inspect(response)}`, - ].join(' '), - ); + /** + * 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(['getPeople'], 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 :: getPeople] 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(['getPeople'], response); - } + if (options && options.resourceMiddleware && options.resourceMiddleware.after) { + _response = await options.resourceMiddleware.after(['getPeople'], _response); + } + + return _response; + })(resourceArgs); if (!(response instanceof Error)) { } @@ -698,6 +729,8 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade >, 0, >, + // This third argument is the cache key type. Since we use objectHash in cacheKeyOptions, this is "string". + string, >( /** * =============================================================== @@ -708,7 +741,7 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade * * ```json * { - * "docsLink": "https://swapi.co/documentation#vehicles", + * "docsLink": "https://swapi.dev/documentation#vehicles", * "isBatchResource": true, * "batchKey": "vehicle_ids", * "newKey": "vehicle_id" @@ -716,14 +749,13 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade * ``` */ async keys => { - if (typeof resources.getVehicles !== 'function') { - return Promise.reject( - [ - '[dataloader-codegen :: getVehicles] resources.getVehicles is not a function.', - 'Did you pass in an instance of getVehicles to "getLoaders"?', - ].join(' '), - ); - } + invariant( + typeof resources.getVehicles === 'function', + [ + '[dataloader-codegen :: getVehicles] resources.getVehicles is not a function.', + 'Did you pass in an instance of getVehicles to "getLoaders"?', + ].join(' '), + ); /** * Chunk up the "keys" array to create a set of "request groups". @@ -803,49 +835,61 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade */ const requests = requestIDs.map(id => keys[id]); - let resourceArgs = [ + // 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], 'vehicle_id'), ['vehicle_ids']: requests.map(k => k['vehicle_id']), }, ]; - if (options && options.resourceMiddleware && options.resourceMiddleware.before) { - resourceArgs = await options.resourceMiddleware.before(['getVehicles'], resourceArgs); - } + let response = await (async _resourceArgs => { + // Make a re-assignable variable so flow/eslint doesn't complain + let __resourceArgs = _resourceArgs; - let response; - try { - // Finally, call the resource! - response = await resources.getVehicles(...resourceArgs); - } catch (error) { - const errorHandler = - options && typeof options.errorHandler === 'function' - ? options.errorHandler - : defaultErrorHandler; + if (options && options.resourceMiddleware && options.resourceMiddleware.before) { + __resourceArgs = await options.resourceMiddleware.before( + ['getVehicles'], + __resourceArgs, + ); + } - /** - * 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(['getVehicles'], error); + let _response; + try { + // Finally, call the resource! + _response = await resources.getVehicles(...__resourceArgs); + } catch (error) { + const errorHandler = + options && typeof options.errorHandler === 'function' + ? options.errorHandler + : defaultErrorHandler; - // Check that errorHandler actually returned an Error object, and turn it into one if not. - if (!(response instanceof Error)) { - response = new Error( - [ - `[dataloader-codegen :: getVehicles] Caught an error, but errorHandler did not return an Error object.`, - `Instead, got ${typeof response}: ${util.inspect(response)}`, - ].join(' '), - ); + /** + * 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(['getVehicles'], 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 :: getVehicles] 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(['getVehicles'], response); - } + if (options && options.resourceMiddleware && options.resourceMiddleware.after) { + _response = await options.resourceMiddleware.after(['getVehicles'], _response); + } + + return _response; + })(resourceArgs); if (!(response instanceof Error)) { } @@ -940,6 +984,8 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade ExtractPromisedReturnValue<[$Call]>]>, $PropertyType, >, + // This third argument is the cache key type. Since we use objectHash in cacheKeyOptions, this is "string". + string, >( /** * =============================================================== @@ -950,28 +996,73 @@ export default function getLoaders(resources: ResourcesType, options?: DataLoade * * ```json * { - * "docsLink": "https://swapi.co/documentation#root", + * "docsLink": "https://swapi.dev/documentation#root", * "isBatchResource": false * } * ``` */ async keys => { - const response = await Promise.all( - keys.map(key => { - if (typeof resources.getRoot !== 'function') { - return Promise.reject( - [ - '[dataloader-codegen :: getRoot] resources.getRoot is not a function.', - 'Did you pass in an instance of getRoot to "getLoaders"?', - ].join(' '), - ); - } + const responses = await Promise.all( + keys.map(async key => { + invariant( + typeof resources.getRoot === 'function', + [ + '[dataloader-codegen :: getRoot] resources.getRoot is not a function.', + 'Did you pass in an instance of getRoot to "getLoaders"?', + ].join(' '), + ); + + // 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 = [key]; + + return 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(['getRoot'], __resourceArgs); + } + + let _response; + try { + // Finally, call the resource! + _response = await resources.getRoot(...__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(['getRoot'], 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 :: getRoot] 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(['getRoot'], _response); + } - return resources.getRoot(key); + return _response; + })(resourceArgs); }), ); - return response; + return responses; }, { ...cacheKeyOptions, diff --git a/examples/swapi/swapi.dataloader-config.yaml b/examples/swapi/swapi.dataloader-config.yaml index 0f117d1..6694107 100644 --- a/examples/swapi/swapi.dataloader-config.yaml +++ b/examples/swapi/swapi.dataloader-config.yaml @@ -8,20 +8,20 @@ typings: resources: getPlanets: - docsLink: https://swapi.co/documentation#planets + docsLink: https://swapi.dev/documentation#planets isBatchResource: true batchKey: planet_ids newKey: planet_id getPeople: - docsLink: https://swapi.co/documentation#people + docsLink: https://swapi.dev/documentation#people isBatchResource: true batchKey: people_ids newKey: person_id getVehicles: - docsLink: https://swapi.co/documentation#vehicles + docsLink: https://swapi.dev/documentation#vehicles isBatchResource: true batchKey: vehicle_ids newKey: vehicle_id getRoot: - docsLink: https://swapi.co/documentation#root + docsLink: https://swapi.dev/documentation#root isBatchResource: false diff --git a/examples/swapi/swapi.js b/examples/swapi/swapi.js index ac10974..e5092a9 100644 --- a/examples/swapi/swapi.js +++ b/examples/swapi/swapi.js @@ -1,11 +1,11 @@ /** - * Clientlib for a subset of data in https://swapi.co/ + * Clientlib for a subset of data in https://swapi.dev/ * @flow */ const url = require('url'); const fetch = require('node-fetch').default; -const SWAPI_URL = 'https://swapi.co/api/'; +const SWAPI_URL = 'https://swapi.dev/api/'; export type SWAPI_Planet = $ReadOnly<{| name: string, diff --git a/examples/swapi/yarn.lock b/examples/swapi/yarn.lock index 28e14d0..333bee5 100644 --- a/examples/swapi/yarn.lock +++ b/examples/swapi/yarn.lock @@ -764,10 +764,10 @@ find-up@^3.0.0: dependencies: locate-path "^3.0.0" -flow-bin@0.122.0: - version "0.122.0" - resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.122.0.tgz#c723a2b33b1a70bd10204704ae1dc776d5d89d79" - integrity sha512-my8N5jgl/A+UVby9E7NDppHdhLgRbWgKbmFZSx2MSYMRh3d9YGnM2MM+wexpUpl0ftY1IM6ZcUwaAhrypLyvlA== +flow-bin@0.123.0: + version "0.123.0" + resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.123.0.tgz#7ba61a0b8775928cf4943ccf78eed2b1b05f7b3a" + integrity sha512-Ylcf8YDIM/KrqtxkPuq+f8O+6sdYA2Nuz5f+sWHlp539DatZz3YMcsO1EiXaf1C11HJgpT/3YGYe7xZ9/UZmvQ== flow-typed@^2.6.2: version "2.6.2" diff --git a/src/genTypeFlow.ts b/src/genTypeFlow.ts index 14f621b..fed276f 100644 --- a/src/genTypeFlow.ts +++ b/src/genTypeFlow.ts @@ -98,7 +98,12 @@ export function getLoaderType(resourceConfig: ResourceConfig, resourcePath: Read const key = getLoaderTypeKey(resourceConfig, resourcePath); const val = getLoaderTypeVal(resourceConfig, resourcePath); - return `DataLoader<${key}, ${val}>`; + return `DataLoader< + ${key}, + ${val}, + // This third argument is the cache key type. Since we use objectHash in cacheKeyOptions, this is "string". + string, + >`; } export function getLoadersTypeMap( diff --git a/src/implementation.ts b/src/implementation.ts index 0d0e02f..8a07cfb 100644 --- a/src/implementation.ts +++ b/src/implementation.ts @@ -24,6 +24,62 @@ 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 ` + (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( + ${JSON.stringify(resourcePath)}, + __resourceArgs + ); + } + + let _response; + try { + // Finally, call the resource! + _response = await ${resourceReference}(...__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(${JSON.stringify(resourcePath)}, error); + + // Check that errorHandler actually returned an Error object, and turn it into one if not. + if (!(_response instanceof Error)) { + _response = new Error([ + \`${errorPrefix( + resourcePath, + )} 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( + ${JSON.stringify(resourcePath)}, + _response + ); + } + + return _response; + }) + `; +} + function getBatchLoader(resourceConfig: BatchResourceConfig, resourcePath: ReadonlyArray) { assert( resourceConfig.isBatchResource === true, @@ -36,14 +92,14 @@ function getBatchLoader(resourceConfig: BatchResourceConfig, resourcePath: Reado return `\ new DataLoader< ${getLoaderTypeKey(resourceConfig, resourcePath)}, - ${getLoaderTypeVal(resourceConfig, resourcePath)} + ${getLoaderTypeVal(resourceConfig, resourcePath)}, + // This third argument is the cache key type. Since we use objectHash in cacheKeyOptions, this is "string". + string, >(${getLoaderComment(resourceConfig, resourcePath)} async (keys) => { - if (typeof ${resourceReference} !== 'function') { - return Promise.reject([ - '${errorPrefix(resourcePath)} ${resourceReference} is not a function.', - 'Did you pass in an instance of ${resourcePath.join('.')} to "getLoaders"?', - ].join(' ')); - } + invariant(typeof ${resourceReference} === 'function', [ + '${errorPrefix(resourcePath)} ${resourceReference} is not a function.', + 'Did you pass in an instance of ${resourcePath.join('.')} to "getLoaders"?', + ].join(' ')); /** * Chunk up the "keys" array to create a set of "request groups". @@ -131,53 +187,17 @@ function getBatchLoader(resourceConfig: BatchResourceConfig, resourcePath: Reado } return ` - let resourceArgs = [{ + // 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}, }]; - - if (options && options.resourceMiddleware && options.resourceMiddleware.before) { - resourceArgs = await options.resourceMiddleware.before( - ${JSON.stringify(resourcePath)}, - resourceArgs - ); - } - - let response; - try { - // Finally, call the resource! - response = await ${resourceReference}(...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(${JSON.stringify(resourcePath)}, error); - - // Check that errorHandler actually returned an Error object, and turn it into one if not. - if (!(response instanceof Error)) { - response = new Error([ - \`${errorPrefix( - resourcePath, - )} 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( - ${JSON.stringify(resourcePath)}, - response - ); - } `; })()} + let response = await ${callResource(resourceConfig, resourcePath)}(resourceArgs); + if (!(response instanceof Error)) { ${(() => { if (typeof resourceConfig.nestedPath === 'string') { @@ -394,20 +414,24 @@ function getNonBatchLoader(resourceConfig: NonBatchResourceConfig, resourcePath: return `\ new DataLoader< ${getLoaderTypeKey(resourceConfig, resourcePath)}, - ${getLoaderTypeVal(resourceConfig, resourcePath)} + ${getLoaderTypeVal(resourceConfig, resourcePath)}, + // This third argument is the cache key type. Since we use objectHash in cacheKeyOptions, this is "string". + string, >(${getLoaderComment(resourceConfig, resourcePath)} async (keys) => { - const response = await Promise.all(keys.map(key => { - if (typeof ${resourceReference} !== 'function') { - return Promise.reject([ - '${errorPrefix(resourcePath)} ${resourceReference} is not a function.', - 'Did you pass in an instance of ${resourcePath.join('.')} to "getLoaders"?', - ].join(' ')); - } + const responses = await Promise.all(keys.map(async key => { + invariant(typeof ${resourceReference} === 'function', [ + '${errorPrefix(resourcePath)} ${resourceReference} is not a function.', + 'Did you pass in an instance of ${resourcePath.join('.')} to "getLoaders"?', + ].join(' ')); + + // 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 = [key]; - return ${resourceReference}(key); + return await ${callResource(resourceConfig, resourcePath)}(resourceArgs); })); - return response; + return responses; }, { ...cacheKeyOptions