-
-
Notifications
You must be signed in to change notification settings - Fork 822
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 16 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 |
---|---|---|
|
@@ -191,8 +191,105 @@ GraphQL resolve info of the current resolver. Used to get the query that starts | |
|
||
[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. | ||
|
||
<h3 id="createDocument">createDocument</h3> | ||
|
||
The `createDocument` 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 | ||
createDocument( | ||
targetSchema: GraphQLSchema, | ||
targetOperation: 'query' | 'mutation' | 'subscription', | ||
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 } = createDocument( | ||
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 `createDocument` 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 } = createDocumentResult | ||
|
||
graphql( | ||
schema, | ||
print(document), | ||
rootValue, | ||
context, | ||
variables | ||
) | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,55 +1,117 @@ | ||
import { | ||
ArgumentNode, | ||
DocumentNode, | ||
FieldNode, | ||
FragmentDefinitionNode, | ||
ArgumentNode, | ||
Kind, | ||
OperationDefinitionNode, | ||
SelectionSetNode, | ||
SelectionNode, | ||
subscribe, | ||
execute, | ||
validate, | ||
VariableDefinitionNode, | ||
GraphQLSchema, | ||
GraphQLResolveInfo | ||
} from 'graphql'; | ||
import { Operation, Request, IDelegateToSchemaOptions } from '../Interfaces'; | ||
import { | ||
applyRequestTransforms, | ||
applyResultTransforms, | ||
} from '../transforms/transforms'; | ||
import { Request, Transform, IDelegateToSchemaOptions, OperationRootDefinition } from '../Interfaces'; | ||
import { applyRequestTransforms, 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 default async function delegateToSchema( | ||
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, | ||
export function createDocument( | ||
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: rawDocument, | ||
variables: info.variableValues as Record<string, any>, | ||
document, | ||
variables: documentInfo.variableValues as Record<string, any>, | ||
}; | ||
|
||
const transforms = [ | ||
...(options.transforms || []), | ||
AddArgumentsAsVariables(options.schema, args), | ||
FilterToSchema(options.schema), | ||
AddTypenameToAbstract(options.schema), | ||
CheckResultAndHandleErrors(info, options.fieldName), | ||
transforms = [ | ||
...(transforms || []), | ||
AddArgumentsAsVariables(targetSchema, roots), | ||
FilterToSchema(targetSchema), | ||
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'm pretty damn proud this still works without any problem :D |
||
AddTypenameToAbstract(targetSchema) | ||
]; | ||
|
||
const processedRequest = applyRequestTransforms(rawRequest, transforms); | ||
return applyRequestTransforms(rawRequest, transforms); | ||
} | ||
|
||
export default async function delegateToSchema( | ||
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. Pet peeve - could you move default export on top? |
||
options: IDelegateToSchemaOptions, | ||
): Promise<any> { | ||
const processedRequest = createDocument( | ||
options.schema, | ||
options.operation, | ||
[ | ||
{ | ||
fieldName: options.fieldName, | ||
args: options.args || {}, | ||
info: options.info | ||
} | ||
], | ||
options.info, | ||
options.transforms | ||
); | ||
|
||
const errors = validate(options.schema, processedRequest.document); | ||
if (errors.length > 0) { | ||
|
@@ -61,11 +123,14 @@ export default async function delegateToSchema( | |
await execute( | ||
options.schema, | ||
processedRequest.document, | ||
info.rootValue, | ||
options.info.rootValue, | ||
options.context, | ||
processedRequest.variables, | ||
), | ||
transforms, | ||
[ | ||
...(options.transforms || []), | ||
CheckResultAndHandleErrors(options.info, options.fieldName), | ||
] | ||
); | ||
} | ||
|
||
|
@@ -74,63 +139,9 @@ export default async function delegateToSchema( | |
return subscribe( | ||
options.schema, | ||
processedRequest.document, | ||
info.rootValue, | ||
options.info.rootValue, | ||
options.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.
Can we just call these
schema
andoperation
, as we do withdelegateToSchema
?