-
-
Notifications
You must be signed in to change notification settings - Fork 828
createRequest
with transforms API
#724
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
Changes from 4 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 |
---|---|---|
@@ -1,6 +1,4 @@ | ||
import { | ||
ArgumentNode, | ||
DocumentNode, | ||
FieldNode, | ||
FragmentDefinitionNode, | ||
Kind, | ||
|
@@ -11,8 +9,10 @@ import { | |
execute, | ||
validate, | ||
VariableDefinitionNode, | ||
GraphQLSchema, | ||
} from 'graphql'; | ||
import { Operation, Request, IDelegateToSchemaOptions } from '../Interfaces'; | ||
import { FetcherOperation } from './makeRemoteExecutableSchema'; | ||
import { Request, Transform, IDelegateToSchemaOptions } from '../Interfaces'; | ||
import { | ||
applyRequestTransforms, | ||
applyResultTransforms, | ||
|
@@ -22,113 +22,148 @@ 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 createBatchOperation( | ||
targetSchema: GraphQLSchema, | ||
targetOperation: 'query' | 'mutation' | 'subscription', | ||
rootDefs: { [key: string]: [{ [key: string]: any }, { [key: string]: any }] }, | ||
graphqlContext: { [key: string]: any }, | ||
documentInfo: { | ||
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. Extract this to a type. 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. Disregard that. Let's always pass info through rootDefs. Having two sources of truth is confusing. 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. Also you don't have 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. It's kinda complicated. You might need to add fragment aliasing. But then we probably don't want to duplicate identical fragments. Same with variables. 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. Yeah i always meant for that to be the full info object, but i just created a new type from the fields i was using. I'll update that 😄 |
||
operation: { | ||
name?: { [key: string]: any } | ||
variableDefinitions?: Array<VariableDefinitionNode>, | ||
}, | ||
variableValues?: { [variableName: string]: any }, | ||
fragments?: { [fragmentName: string]: FragmentDefinitionNode }, | ||
}, | ||
transforms?: Array<Transform>, | ||
): FetcherOperation { | ||
const roots = Object.keys(rootDefs).map(key => { | ||
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. Question here is whether we should auto-alias stuff or if we should expect user to alias themselves. I guess latter. Data-loading API can then alias by itself. 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 was thinking users should add aliasing themselves: {
'user1:node': [{ id: 1 }, ...],
'user2:node': [{ id: 2 }, ...]
} But obviously with the API you specified below (: |
||
const [args, info] = rootDefs[key]; | ||
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. Ok, this starts getting very complicated. How about this as a signature for rootdefs:
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. Also extract it to a type. 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. Yeah that works for me! |
||
const [a, n] = key.split(':'); | ||
const name = n || a; | ||
const alias = n ? a : null; | ||
return { | ||
key: alias || name, | ||
name, | ||
alias, | ||
args, | ||
info: info || documentInfo | ||
}; | ||
}); | ||
|
||
const selections: Array<SelectionNode> = roots.reduce((newSelections, { key, name: rootFieldName, info, alias, args }) => { | ||
const rootSelections = info.fieldNodes.map((selection: FieldNode) => { | ||
if (selection.kind === Kind.FIELD) { | ||
const rootField: FieldNode = { | ||
kind: Kind.FIELD, | ||
name: { | ||
kind: Kind.NAME, | ||
value: rootFieldName, | ||
}, | ||
alias: alias | ||
? { | ||
kind: Kind.NAME, | ||
value: alias | ||
} | ||
: null, | ||
arguments: selection.arguments, | ||
selectionSet: selection.selectionSet | ||
}; | ||
return rootField; | ||
} | ||
return selection; | ||
}); | ||
|
||
return newSelections.concat(rootSelections); | ||
}, []); | ||
|
||
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 || []), | ||
...roots.map(({ args }) => AddArgumentsAsVariables(targetSchema, args)), | ||
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. This will reset a variable counter each time. I think you should have one 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. This way we don't need to traverse AST that many times. 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. @freiksenet do you think that parameter's type should be: args: { [key: string]: any } | Array<{ fieldName: string, alias?: string, args: { [key: string]: any } }> ? |
||
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); | ||
const { document: query, variables } = applyRequestTransforms(rawRequest, transforms); | ||
|
||
const errors = validate(options.schema, processedRequest.document); | ||
return { | ||
query, | ||
variables, | ||
context: graphqlContext, | ||
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. Should we return 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. Or should we remove context all together, and just have this function return 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. Just document and variables I think. |
||
operationName: documentInfo.operation && documentInfo.operation.name && documentInfo.operation.name.value | ||
}; | ||
} | ||
|
||
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 = createBatchOperation( | ||
options.schema, | ||
options.operation, | ||
{ | ||
[options.fieldName]: [options.args || {}, options.info] | ||
}, | ||
options.context, | ||
options.info, | ||
options.transforms | ||
); | ||
|
||
const errors = validate(options.schema, processedRequest.query); | ||
if (errors.length > 0) { | ||
throw errors; | ||
} | ||
|
||
if (options.operation === 'query' || | ||
options.operation === 'mutation') { | ||
return applyResultTransforms(await execute( | ||
if (options.operation === 'query' || options.operation === 'mutation') { | ||
const rawResult = await execute( | ||
options.schema, | ||
processedRequest.document, | ||
info.rootValue, | ||
processedRequest.query, | ||
options.info.rootValue, | ||
options.context, | ||
processedRequest.variables, | ||
), transforms); | ||
); | ||
|
||
const result = applyResultTransforms(rawResult, [ | ||
...(options.transforms || []), | ||
CheckResultAndHandleErrors(options.info, options.fieldName), | ||
]); | ||
|
||
return result; | ||
} | ||
|
||
if (options.operation === 'subscription') { | ||
// apply result processing ??? | ||
return subscribe( | ||
options.schema, | ||
processedRequest.document, | ||
info.rootValue, | ||
processedRequest.query, | ||
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.
We can probably name it
createDocument
.createDocument
was never a public API and this probably should be.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.
What do you think about
createOperation
? Since I think it's good to return the variables too.