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

Support for @skip and @include #275

Merged
merged 21 commits into from
Jun 21, 2016
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 182 additions & 0 deletions src/queries/directives.ts
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';
Copy link
Contributor

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?

Copy link
Contributor

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


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,
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

@Poincare Poincare Jun 13, 2016

Choose a reason for hiding this comment

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

The DirectiveResolver only applies to instances of Selection which do have directives in the AST (e.g. in this query). The point of having this type is so that we can extend directives easily once they exist for things other than just Field instances.

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) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Wouldn't filter be appropriate here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Although the directives @skip and @include currently act as filters, there is some intention of supporting directives that do stuff other than what @skip and @include do (e.g. this issue on graphql-js mentions some custom directives that don't exclude or include fields).


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];
Copy link
Contributor

Choose a reason for hiding this comment

The 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,
});
}
Loading