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
Changes from 27 commits
f9c46be
33dcd04
db3a233
b3a97c6
34d4eb5
5552a3e
02db0a4
251d0dc
8c5e3c8
a726453
a98d8cc
6b28177
242cde0
a8704ac
fade3fb
4be53ed
5bb040b
6592810
11697ae
e657fae
ce7a18d
eeb1a84
bbc0265
30b9119
188442d
88407e4
1b8cafd
8fd2a46
9e92ba6
5f46b3e
779100b
fed0f01
948355e
a61afae
ffd5f4c
a8e5038
2a8e82c
536805b
e08cc10
5607de4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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: | ||
|
||
```js | ||
createRequest( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we just call these |
||
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 }, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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") {...}
} There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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 | ||
) | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
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[], | ||
|
@@ -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], | ||
}; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Slight rewording?