-
Notifications
You must be signed in to change notification settings - Fork 2.7k
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
Support for @skip
and @include
#275
Changes from 3 commits
d89d14d
948618d
f53939f
5588db1
46dab91
7e1395a
28b9a41
ca7518b
8870462
f84f45f
20c1ea4
7cd95d5
0c1e74c
e2b410d
1cd9f9f
bced2d4
4bda736
a451fd2
5df4a87
132f2e7
85a7585
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 |
---|---|---|
@@ -0,0 +1,182 @@ | ||
// Provides the methods that allow QueryManager to handle | ||
// the `skip` and `include` directives within GraphQL. | ||
|
||
import { | ||
SelectionSet, | ||
Directive, | ||
Selection, | ||
Document, | ||
InlineFragment, | ||
Field, | ||
BooleanValue, | ||
Variable, | ||
} from 'graphql'; | ||
|
||
import { | ||
getQueryDefinition, | ||
getFragmentDefinitions, | ||
} from './getFromAST'; | ||
|
||
import identity = require('lodash.identity'); | ||
import cloneDeep = require('lodash.clonedeep'); | ||
|
||
// A handler that takes a selection, variables, and a directive to apply to that selection. | ||
export type DirectiveResolver = (selection: Selection, | ||
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. Why do we need a type for this? A directive always applies to a single node/field, but DirectiveResolvers apply to selection sets, which I think makes things more complicated. 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. The |
||
variables: { [name: string]: any }, | ||
directive: Directive) => Selection | ||
|
||
export function applyDirectives(doc: Document, | ||
variables?: { [name: string]: any }, | ||
directiveResolvers?: { [name: string]: DirectiveResolver} ) | ||
: Document { | ||
if (!variables) { | ||
variables = {}; | ||
} | ||
if (!directiveResolvers) { | ||
directiveResolvers = {}; | ||
} | ||
|
||
const newDoc = cloneDeep(doc); | ||
const fragmentDefs = getFragmentDefinitions(newDoc); | ||
const queryDef = getQueryDefinition(newDoc); | ||
const newSelSet = applyDirectivesToSelectionSet(queryDef.selectionSet, | ||
variables, | ||
directiveResolvers); | ||
queryDef.selectionSet = newSelSet; | ||
newDoc.definitions = fragmentDefs.map((fragmentDef) => { | ||
const fragmentSelSet = applyDirectivesToSelectionSet(fragmentDef.selectionSet, | ||
variables, | ||
directiveResolvers); | ||
fragmentDef.selectionSet = fragmentSelSet; | ||
return fragmentDef; | ||
}); | ||
newDoc.definitions.unshift(queryDef); | ||
|
||
return newDoc; | ||
} | ||
|
||
export function applyDirectivesToSelectionSet(selSet: SelectionSet, | ||
variables: { [name: string]: any }, | ||
directiveResolvers: { | ||
[name: string]: DirectiveResolver}) | ||
: SelectionSet { | ||
|
||
const selections = selSet.selections; | ||
selSet.selections = selections.map((selection) => { | ||
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. Wouldn't 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. Although the directives |
||
|
||
let newSelection: Selection = selection; | ||
let currSelection: Selection = selection; | ||
let toBeRemoved = null; | ||
|
||
selection.directives.forEach((directive) => { | ||
if (directive.name && directive.name.value) { | ||
const directiveResolver = directiveResolvers[directive.name.value]; | ||
newSelection = directiveResolver(currSelection, variables, directive); | ||
|
||
// add handling for the case where we have both a skip and an include | ||
// on the same field (see note here: http://facebook.github.io/graphql/#sec--include). | ||
if (directive.name.value === 'skip' || directive.name.value === 'include') { | ||
if (newSelection === undefined && toBeRemoved === null) { | ||
toBeRemoved = true; | ||
} else if (newSelection === undefined) { | ||
currSelection = selection; | ||
} | ||
if (newSelection) { | ||
toBeRemoved = false; | ||
currSelection = newSelection; | ||
} | ||
} | ||
} | ||
}); | ||
|
||
if (newSelection !== undefined) { | ||
const withSelSet = selection as (InlineFragment | Field); | ||
// recursively operate on selection sets within this selection set. | ||
if (withSelSet.kind === 'InlineFragment' || | ||
(withSelSet.kind === 'Field' && withSelSet.selectionSet)) { | ||
withSelSet.selectionSet = applyDirectivesToSelectionSet(withSelSet.selectionSet, | ||
variables, | ||
directiveResolvers); | ||
} | ||
return newSelection; | ||
} else if (!toBeRemoved) { | ||
return currSelection; | ||
} | ||
}); | ||
|
||
//filter out undefined values | ||
selSet.selections = selSet.selections.filter(identity); | ||
return selSet; | ||
} | ||
|
||
export function skipIncludeDirectiveResolver(directiveName: string, | ||
selection: Selection, | ||
variables: { [name: string]: any }, | ||
directive: Directive) | ||
: Selection { | ||
//evaluate the "if" argument and skip (i.e. return undefined) if it evaluates to true. | ||
const directiveArguments = directive.arguments; | ||
if (directiveArguments.length !== 1) { | ||
throw new Error(`Incorrect number of arguments for the @$(directiveName} directive.`); | ||
} | ||
|
||
const ifArgument = directive.arguments[0]; | ||
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 good to do sanity checks, but I'm not sure they're necessary in this case. We might also just delegate that to GraphQL validation, which I'm pretty sure apollo client will include at some point in the future. For the time being, I think it's fine not to provide great error messages, because it will simplify the code a lot. |
||
if (!ifArgument.name || ifArgument.name.value !== 'if') { | ||
throw new Error(`Invalid argument for the @${directiveName} directive.`); | ||
} | ||
|
||
const ifValue = directive.arguments[0].value; | ||
let evaledValue: Boolean = false; | ||
if (!ifValue || ifValue.kind !== 'BooleanValue') { | ||
// means it has to be a variable value if this is a valid @skip directive | ||
if (ifValue.kind !== 'Variable') { | ||
throw new Error(`Invalid argument value for the @${directiveName} directive.`); | ||
} else { | ||
evaledValue = variables[(ifValue as Variable).name.value]; | ||
if (evaledValue === undefined) { | ||
throw new Error(`Invalid variable referenced in @${directiveName} directive.`); | ||
} | ||
} | ||
} else { | ||
evaledValue = (ifValue as BooleanValue).value; | ||
} | ||
|
||
if (directiveName === 'skip') { | ||
evaledValue = !evaledValue; | ||
} | ||
|
||
// if the value is false, then don't skip it. | ||
if (evaledValue) { | ||
return selection; | ||
} else { | ||
return undefined; | ||
} | ||
} | ||
|
||
export function skipDirectiveResolver(selection: Selection, | ||
variables: { [name: string]: any }, | ||
directive: Directive) | ||
: Selection { | ||
return skipIncludeDirectiveResolver('skip', selection, variables, directive); | ||
} | ||
|
||
export function includeDirectiveResolver(selection: Selection, | ||
variables: { [name: string]: any }, | ||
directive: Directive) | ||
: Selection { | ||
return skipIncludeDirectiveResolver('include', selection, variables, directive); | ||
} | ||
|
||
export function applySkipResolver(doc: Document, variables?: { [name: string]: any }) | ||
: Document { | ||
return applyDirectives(doc, variables, { | ||
'skip': skipDirectiveResolver, | ||
}); | ||
} | ||
|
||
export function applyIncludeResolver(doc: Document, variables?: { [name: string]: any }) | ||
: Document { | ||
return applyDirectives(doc, variables, { | ||
'include': includeDirectiveResolver, | ||
}); | ||
} |
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.
Does this import all of graphql now, because the exact path isn't specified?
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.
These are just the types, it doesn't actually import anything afaik