Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Helper + docs for selectionSet arguments #1802

Merged
merged 8 commits into from
Jul 21, 2020
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/stitch/src/index.ts
@@ -1 +1,2 @@
export { stitchSchemas } from './stitchSchemas';
export { forwardArgsToSelectionSet } from './selectionSetArgs';
29 changes: 29 additions & 0 deletions 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<string, string[]>
) => (field: FieldNode) => SelectionSetNode = (selectionSet: string, mapping?: Record<string, string[]>) => {
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 };
};
};
33 changes: 33 additions & 0 deletions 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');
});
});
73 changes: 73 additions & 0 deletions website/docs/schema-stitching.md
Expand Up @@ -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 `SelectionSet`. 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.)
Expand Down