From ad349394407447639f1b896346f71afca22ad9de Mon Sep 17 00:00:00 2001 From: Greg MacWilliam Date: Tue, 21 Jul 2020 10:12:26 -0400 Subject: [PATCH] Helper + docs for selectionSet arguments (#1802) * helper for selection set with arguments. * add tests and docs. * add missing type declaration. * fix GraphQL v15 test. * new selectionSet via shallow copies. * fix typing? * refactor args helper. * fix type name in documentation. --- packages/stitch/src/index.ts | 1 + packages/stitch/src/selectionSetArgs.ts | 29 ++++++++ .../stitch/tests/selectionSetArgs.test.ts | 33 +++++++++ website/docs/schema-stitching.md | 73 +++++++++++++++++++ 4 files changed, 136 insertions(+) create mode 100644 packages/stitch/src/selectionSetArgs.ts create mode 100644 packages/stitch/tests/selectionSetArgs.test.ts diff --git a/packages/stitch/src/index.ts b/packages/stitch/src/index.ts index 8ded856905c..5aa711ccfb4 100755 --- a/packages/stitch/src/index.ts +++ b/packages/stitch/src/index.ts @@ -1 +1,2 @@ export { stitchSchemas } from './stitchSchemas'; +export { forwardArgsToSelectionSet } from './selectionSetArgs'; diff --git a/packages/stitch/src/selectionSetArgs.ts b/packages/stitch/src/selectionSetArgs.ts new file mode 100644 index 00000000000..2e5f583d934 --- /dev/null +++ b/packages/stitch/src/selectionSetArgs.ts @@ -0,0 +1,29 @@ +import { parseSelectionSet } from '@graphql-tools/utils'; +import { SelectionSetNode, SelectionNode, FieldNode, Kind } from 'graphql'; + +export const forwardArgsToSelectionSet: ( + selectionSet: string, + mapping?: Record +) => (field: FieldNode) => SelectionSetNode = (selectionSet: string, mapping?: Record) => { + const selectionSetDef = parseSelectionSet(selectionSet); + return (field: FieldNode): SelectionSetNode => { + const selections = selectionSetDef.selections.map( + (selectionNode): SelectionNode => { + if (selectionNode.kind === Kind.FIELD) { + if (!mapping) { + return { ...selectionNode, arguments: field.arguments.slice() }; + } else if (selectionNode.name.value in mapping) { + const selectionArgs = mapping[selectionNode.name.value]; + return { + ...selectionNode, + arguments: field.arguments.filter((arg): boolean => selectionArgs.includes(arg.name.value)), + }; + } + } + return selectionNode; + } + ); + + return { ...selectionSetDef, selections }; + }; +}; diff --git a/packages/stitch/tests/selectionSetArgs.test.ts b/packages/stitch/tests/selectionSetArgs.test.ts new file mode 100644 index 00000000000..0397a21c0dc --- /dev/null +++ b/packages/stitch/tests/selectionSetArgs.test.ts @@ -0,0 +1,33 @@ +import { parseSelectionSet } from '@graphql-tools/utils'; +import { forwardArgsToSelectionSet } from '../src'; + +describe('forwardArgsToSelectionSet', () => { + + const GATEWAY_FIELD = parseSelectionSet('{ posts(pageNumber: 1, perPage: 7) }').selections[0]; + + test('passes all arguments to a hint selection set', () => { + const buildSelectionSet = forwardArgsToSelectionSet('{ postIds }'); + const result = buildSelectionSet(GATEWAY_FIELD).selections[0]; + + expect(result.name.value).toEqual('postIds'); + expect(result.arguments.length).toEqual(2); + expect(result.arguments[0].name.value).toEqual('pageNumber'); + expect(result.arguments[0].value.value).toEqual('1'); + expect(result.arguments[1].name.value).toEqual('perPage'); + expect(result.arguments[1].value.value).toEqual('7'); + }); + + test('passes mapped arguments to a hint selection set', () => { + const buildSelectionSet = forwardArgsToSelectionSet('{ id postIds }', { postIds: ['pageNumber'] }); + const result = buildSelectionSet(GATEWAY_FIELD); + + expect(result.selections.length).toEqual(2); + expect(result.selections[0].name.value).toEqual('id'); + expect(result.selections[0].arguments.length).toEqual(0); + + expect(result.selections[1].name.value).toEqual('postIds'); + expect(result.selections[1].arguments.length).toEqual(1); + expect(result.selections[1].arguments[0].name.value).toEqual('pageNumber'); + expect(result.selections[1].arguments[0].value.value).toEqual('1'); + }); +}); diff --git a/website/docs/schema-stitching.md b/website/docs/schema-stitching.md index 3af89423b24..c6fc2a1e738 100644 --- a/website/docs/schema-stitching.md +++ b/website/docs/schema-stitching.md @@ -167,6 +167,79 @@ const schema = stitchSchemas({ }); ``` +### Passing arguments between resolvers + +As a stitching implementation scales, we encounter situations where it's impractical to pass around exhaustive sets of records between services. For example, what if a User has tens of thousands of Chirps? We'll want to add scoping arguments into our gateway schema to address this: + +```js +const linkTypeDefs = ` + extend type User { + chirps(since: DateTime): [Chirp] + } +`; +``` + +This argument in the gateway schema won't do anything until passed through to the underlying subservice requests. How we pass this input through depends on which subservice manages the association data. + +First, let's say that the Chirps service manages the association and implements a `since` param for scoping the returned Chirp results. This simply requires passing the resolver argument through to `delegateToSchema`: + +```js +export default { + User: { + chirps: { + selectionSet: `{ id }`, + resolve(user, args, context, info) { + return delegateToSchema({ + schema: chirpSchema, + operation: 'query', + fieldName: 'chirpsByAuthorId', + args: { + authorId: user.id, + since: args.since + }, + context, + info, + }); + }, + }, + } +}; +``` + +Alternatively, let's say that the Users service manages the association and implements a `User.chirpIds(since: DateTime):[Int]` method to stitch from. In this configuration, resolver arguments will need to passthrough with the initial `selectionSet` for User data. The `forwardArgsToSelectionSet` helper handles this: + +```js +import { forwardArgsToSelectionSet } from '@graphql-tools/stitch'; + +export default { + User: { + chirps: { + selectionSet: forwardArgsToSelectionSet('{ chirpIds }'), + resolve(user, args, context, info) { + return delegateToSchema({ + schema: chirpSchema, + operation: 'query', + fieldName: 'chirpsById', + args: { + ids: user.chirpIds, + }, + context, + info, + }); + }, + }, + } +}; +``` + +By default, `forwardArgsToSelectionSet` will passthrough all arguments from the gateway field to _all_ root fields in the selection set. For complex selections that request multiple fields, you may provide an additional mapping of selection names with their respective arguments: + +```js +forwardArgsToSelectionSet('{ id chirpIds }', { chirpIds: ['since'] }) +``` + +Note that a dynamic `selectionSet` is simply a function that recieves a GraphQL `FieldNode` (the gateway field) and returns a `SelectionSetNode`. This dynamic capability can support a wide range of custom stitching configurations. + ## Using with Transforms Often, when creating a GraphQL gateway that combines multiple existing schemas, we might want to modify one of the schemas. The most common tasks include renaming some of the types, and filtering the root fields. By using [transforms](/docs/schema-transforms/) with schema stitching, we can easily tweak the subschemas before merging them together. (In earlier versions of graphql-tools, this required an additional round of delegation prior to merging, but transforms can now be specifying directly when merging using the new subschema configuration objects.)