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

createRequest with transforms API #724

Closed
wants to merge 40 commits into from
Closed
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
f9c46be
createBatchOperation with transforms API
Apr 9, 2018
33dcd04
Merge branch 'next-api' of git://github.com/apollographql/graphql-too…
Apr 9, 2018
db3a233
Fix tests for adjacent fields in fragments
Apr 9, 2018
b3a97c6
remove createDocument that is no longer used or exported
Apr 9, 2018
34d4eb5
WIP fix createOperation API (thanks freiksenet)
Apr 10, 2018
5552a3e
WIP: address more review comments
Apr 11, 2018
02db0a4
Remove hack that will cause bugs
Apr 12, 2018
251d0dc
Fix tests
Apr 12, 2018
8c5e3c8
Clean up
Apr 12, 2018
a726453
Fix bugs related to passing no arguments
Apr 13, 2018
a98d8cc
Add two tests (let me know if i should move them)
Apr 13, 2018
6b28177
Return `Request` type, createOperation -> createDocument
Apr 17, 2018
242cde0
More tests for createDocument
Apr 17, 2018
a8704ac
Merge branch 'next-api' of git://github.com/apollographql/graphql-too…
Apr 17, 2018
fade3fb
Add execution test
Apr 17, 2018
4be53ed
createDocuments docs
Apr 17, 2018
5bb040b
createDocument -> createRequest
Apr 18, 2018
6592810
Export createRequest
Apr 18, 2018
11697ae
Merge branch 'next-api' of https://github.com/apollographql/graphql-t…
Apr 20, 2018
e657fae
Merge branch 'master' into apollographql-next-api
Apr 24, 2018
ce7a18d
Merge branch 'master' into apollographql-next-api
Apr 27, 2018
eeb1a84
Merge branch 'master' into apollographql-next-api
May 1, 2018
bbc0265
Merge branch 'master' into apollographql-next-api
May 7, 2018
30b9119
Merge branch 'master' into apollographql-next-api
May 7, 2018
188442d
Merge branch 'master' into apollographql-next-api
May 8, 2018
88407e4
Merge branch 'master' into apollographql-next-api
May 15, 2018
1b8cafd
Merge branch 'master' into apollographql-next-api
May 21, 2018
8fd2a46
Address review comments
May 23, 2018
9e92ba6
Reword docs (typo)
May 23, 2018
5f46b3e
Merge branch 'master' into apollographql-next-api
Jun 4, 2018
779100b
Merge branch 'master' into apollographql-next-api
Jun 18, 2018
fed0f01
Merge branch 'master' into apollographql-next-api
Jun 18, 2018
948355e
Merge branch 'master' into apollographql-next-api
Jun 26, 2018
a61afae
Merge branch 'master' into apollographql-next-api
mfix22 Aug 1, 2018
ffd5f4c
Merge branch 'master' into apollographql-next-api
Aug 12, 2018
a8e5038
Merge branch 'master' into apollographql-next-api
Aug 29, 2018
2a8e82c
Merge branch 'master' into apollographql-next-api
hwillson Sep 7, 2018
536805b
Merge branch 'master' into apollographql-next-api
mfix22 Oct 13, 2018
e08cc10
Merge branch 'master' into apollographql-next-api
mfix22 Oct 23, 2018
5607de4
Merge branch 'master' into apollographql-next-api
mfix22 Nov 30, 2018
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
97 changes: 97 additions & 0 deletions docs/source/schema-delegation.md
Expand Up @@ -193,8 +193,105 @@ Also provides the `info.mergeInfo.delegateToSchema` function discussed above.

[Transforms](./schema-transforms.html) to apply to the query and results. Should be the same transforms that were used to transform the schema, if any. After transformation, `transformedSchema.transforms` contains the transforms that were applied.

<h3 id="createRequest">createRequest</h3>

The `createRequest` is a utility function for creating queries with multiple, aliased, roots and possible argument name collisions. The function should be called with these parameters:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Slight rewording?

The createRequest function is a utility for...


```js
createRequest(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use an object of named parameters rather than positional parameters, please.

targetSchema: GraphQLSchema,
targetOperation: 'query' | 'mutation' | 'subscription',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we just call these schema and operation, as we do with delegateToSchema?

roots: Array<OperationRootDefinition>,
documentInfo: GraphQLResolveInfo,
transforms?: Array<Transform>,
): Request
```

where `OperationRootDefinition` is the following:
```js
type OperationRootDefinition = {
fieldName: string,
// string to rename the root fieldName as
alias?: string,
// args passed to the root field
args?: { [key: string]: any },
// contains the `fieldNodes` that will act as the root field's selection set
info?: GraphQLResolveInfo
};
```

#### Example
```js
User: {
bookings(parent, args, context, info) {
const { document, variables } = createRequest(
subschema,
'query',
[
{ fieldName: 'node', alias: 'booking1', args: { id: 'b1' }, info },
{ fieldName: 'node', alias: 'booking2', args: { id: 'b2' }, info },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

General question: why not just construct a query that uses alias syntax?

query QueryName {
  booking1: node(id: "b1") {...}
  booking2: node(id: "b2") {...}
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean that is what this is doing, just programmatically right? This lets users define a batch operation (with many roots) and send that via a fetch etc.

If you just form the query from a string, you are missing all the other operations that the transforms do.

],
info
)
return graphql.execute(
subschema,
document,
{},
context,
variables
).then(result => {
return Object.values(result.data) // turn aliased keys into array of values
})
}
},
```

#### schema: GraphQLSchema

A subschema to get type information from.

#### operation: 'query' | 'mutation' | 'subscription'

An operation to use during the delegation.

#### roots: Array<OperationRootDefinition>

A list of root definitions. This is where you can define multiple root fields for your query, as well as which args should be passed to each, and if they should be aliased or not

##### Example
```js
[
// Info contains your selections, which in this case would be something like `['id']`
{ fieldName: 'node', alias: 'user1', args: { id: '1' }, info },
{ fieldName: 'node', alias: 'user2', args: { id: '2' }, info },
]
```

#### documentInfo: GraphQLResolveInfo

Info object containing fields that are not specific to root fields, but rather the document as a whole, like `fragments` and other `variableValues`

#### transforms: Array<Transform>

[Transforms](./transforms.html) to apply to the query and results. Should be the same transforms that were used to transform the schema, if any. One can use `transformedSchema.transforms` to retrieve transforms.

<h2 id="considerations">Additional considerations</h2>

### Aliases

Delegation preserves aliases that are passed from the parent query. However that presents problems, because default GraphQL resolvers retrieve field from parent based on their name, not aliases. This way results with aliases will be missing from the delegated result. `mergeSchemas` and `transformSchemas` go around that by using `src/stitching/defaultMergedResolver` for all fields without explicit resolver. When building new libraries around delegation, one should consider how the aliases will be handled.

However, to create an aliased query/mutation, you can use `createRequest` and pass the resulting `document` and `variables` into `graphql` (or `execute` or your own fetcher). For example:
```js
import { graphql } from 'graphql'

const { document, variables } = createRequestResult

graphql(
schema,
print(document),
rootValue,
context,
variables
)
```
7 changes: 7 additions & 0 deletions src/Interfaces.ts
Expand Up @@ -200,3 +200,10 @@ export type GraphQLParseOptions = {
allowLegacySDLImplementsInterfaces?: boolean;
experimentalFragmentVariables?: boolean;
};

export type OperationRootDefinition = {
fieldName: string,
alias?: string,
args?: { [key: string]: any },
info?: GraphQLResolveInfo
};
204 changes: 114 additions & 90 deletions src/stitching/delegateToSchema.ts
@@ -1,35 +1,112 @@
import {
ArgumentNode,
DocumentNode,
FieldNode,
FragmentDefinitionNode,
ArgumentNode,
Kind,
OperationDefinitionNode,
SelectionSetNode,
SelectionNode,
subscribe,
execute,
validate,
VariableDefinitionNode,
GraphQLResolveInfo,
GraphQLSchema,
} from 'graphql';

import {
Operation,
Request,
IDelegateToSchemaOptions,
Transform,
OperationRootDefinition,
} from '../Interfaces';

import {
applyRequestTransforms,
applyResultTransforms,
applyResultTransforms
} from '../transforms/transforms';

import AddArgumentsAsVariables from '../transforms/AddArgumentsAsVariables';
import FilterToSchema from '../transforms/FilterToSchema';
import AddTypenameToAbstract from '../transforms/AddTypenameToAbstract';
import CheckResultAndHandleErrors from '../transforms/CheckResultAndHandleErrors';

export function createRequest(
targetSchema: GraphQLSchema,
targetOperation: 'query' | 'mutation' | 'subscription',
roots: Array<OperationRootDefinition>,
documentInfo: GraphQLResolveInfo,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left this for now, but let me know if you want me to change this. documentInfo was a prop i though users could use to define properties that are global to that operation, and do not affect specific root fields. Like fragments, for example.

transforms?: Array<Transform>,
): Request {
const selections: Array<SelectionNode> = roots.map(({ fieldName, info, alias }) => {
const newSelections: Array<SelectionNode> = info
? [].concat(...info.fieldNodes.map((field: FieldNode) => field.selectionSet ? field.selectionSet.selections : []))
: [];

const args: Array<ArgumentNode> = info
? [].concat( ...info.fieldNodes.map((field: FieldNode) => field.arguments || []))
: [];

const rootSelectionSet = newSelections.length > 0
? {
kind: Kind.SELECTION_SET,
selections: newSelections
}
: null;

const rootField: FieldNode = {
kind: Kind.FIELD,
name: {
kind: Kind.NAME,
value: fieldName,
},
alias: alias
? {
kind: Kind.NAME,
value: alias
}
: null,
selectionSet: rootSelectionSet,
arguments: args
};

return rootField;
}, []);

const selectionSet: SelectionSetNode = {
kind: Kind.SELECTION_SET,
selections,
};

const operationDefinition: OperationDefinitionNode = {
kind: Kind.OPERATION_DEFINITION,
operation: targetOperation,
variableDefinitions: documentInfo.operation.variableDefinitions,
selectionSet,
};

const fragments = Object.keys(documentInfo.fragments).map(
fragmentName => documentInfo.fragments[fragmentName],
);

const document = {
kind: Kind.DOCUMENT,
definitions: [operationDefinition, ...fragments],
};

const rawRequest: Request = {
document,
variables: documentInfo.variableValues as Record<string, any>,
};

transforms = [
...(transforms || []),
new AddArgumentsAsVariables(targetSchema, roots),
new FilterToSchema(targetSchema),
new AddTypenameToAbstract(targetSchema),
];

return applyRequestTransforms(rawRequest, transforms);
}

export default function delegateToSchema(
options: IDelegateToSchemaOptions | GraphQLSchema,
...args: any[],
Expand All @@ -46,112 +123,59 @@ export default function delegateToSchema(
async function delegateToSchemaImplementation(
options: IDelegateToSchemaOptions,
): Promise<any> {
const { info, args = {} } = options;
const rawDocument: DocumentNode = createDocument(
options.fieldName,
options.operation,
info.fieldNodes,
Object.keys(info.fragments).map(
fragmentName => info.fragments[fragmentName],
),
info.operation.variableDefinitions,
const {
info,
args = {},
fieldName,
schema,
operation,
context
} = options;
const processedRequest = createRequest(
schema,
operation,
[
{
fieldName,
args,
info
}
],
info,
options.transforms
);

const rawRequest: Request = {
document: rawDocument,
variables: info.variableValues as Record<string, any>,
};
const errors = validate(schema, processedRequest.document);
if (errors.length > 0) {
throw errors;
}

const transforms = [
...(options.transforms || []),
new AddArgumentsAsVariables(options.schema, args),
new FilterToSchema(options.schema),
new AddTypenameToAbstract(options.schema),
new CheckResultAndHandleErrors(info, options.fieldName),
new CheckResultAndHandleErrors(info, fieldName),
];

const processedRequest = applyRequestTransforms(rawRequest, transforms);

const errors = validate(options.schema, processedRequest.document);
if (errors.length > 0) {
throw errors;
}

if (options.operation === 'query' || options.operation === 'mutation') {
if (operation === 'query' || operation === 'mutation') {
return applyResultTransforms(
await execute(
options.schema,
schema,
processedRequest.document,
info.rootValue,
options.context,
context,
processedRequest.variables,
),
transforms,
);
}

if (options.operation === 'subscription') {
if (operation === 'subscription') {
// apply result processing ???
return subscribe(
options.schema,
schema,
processedRequest.document,
info.rootValue,
options.context,
context,
processedRequest.variables,
);
}
}

function createDocument(
targetField: string,
targetOperation: Operation,
originalSelections: Array<SelectionNode>,
fragments: Array<FragmentDefinitionNode>,
variables: Array<VariableDefinitionNode>,
): DocumentNode {
let selections: Array<SelectionNode> = [];
let args: Array<ArgumentNode> = [];

originalSelections.forEach((field: FieldNode) => {
const fieldSelections = field.selectionSet
? field.selectionSet.selections
: [];
selections = selections.concat(fieldSelections);
args = args.concat(field.arguments || []);
});

let selectionSet = null;
if (selections.length > 0) {
selectionSet = {
kind: Kind.SELECTION_SET,
selections: selections,
};
}

const rootField: FieldNode = {
kind: Kind.FIELD,
alias: null,
arguments: args,
selectionSet,
name: {
kind: Kind.NAME,
value: targetField,
},
};
const rootSelectionSet: SelectionSetNode = {
kind: Kind.SELECTION_SET,
selections: [rootField],
};

const operationDefinition: OperationDefinitionNode = {
kind: Kind.OPERATION_DEFINITION,
operation: targetOperation,
variableDefinitions: variables,
selectionSet: rootSelectionSet,
};

return {
kind: Kind.DOCUMENT,
definitions: [operationDefinition, ...fragments],
};
}
3 changes: 2 additions & 1 deletion src/stitching/index.ts
@@ -1,7 +1,7 @@
import makeRemoteExecutableSchema, { createResolver as defaultCreateRemoteResolver } from './makeRemoteExecutableSchema';
import introspectSchema from './introspectSchema';
import mergeSchemas from './mergeSchemas';
import delegateToSchema from './delegateToSchema';
import delegateToSchema, { createRequest } from './delegateToSchema';
import defaultMergedResolver from './defaultMergedResolver';

export {
Expand All @@ -12,5 +12,6 @@ export {
// but exposed for the community use
delegateToSchema,
defaultMergedResolver,
createRequest,
defaultCreateRemoteResolver
};