Skip to content

Commit

Permalink
Helper + docs for selectionSet arguments (#1802)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
Greg MacWilliam committed Jul 21, 2020
1 parent 53f62bd commit ad34939
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/stitch/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { stitchSchemas } from './stitchSchemas';
export { forwardArgsToSelectionSet } from './selectionSetArgs';
29 changes: 29 additions & 0 deletions packages/stitch/src/selectionSetArgs.ts
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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 `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.)
Expand Down

0 comments on commit ad34939

Please sign in to comment.