-
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 7 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,65 @@ | ||
// Provides the methods that allow QueryManager to handle | ||
// the `skip` and `include` directives within GraphQL. | ||
import { | ||
Selection, | ||
Variable, | ||
BooleanValue, | ||
Directive, | ||
} from 'graphql'; | ||
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. 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 commentThe reason will be displayed to describe this comment to others. Learn more. These are just the types, it doesn't actually import anything afaik |
||
|
||
export function evaluateSkipInclude(directive: Directive, variables: { [name: string]: any }): Boolean { | ||
//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 directiveName = directive.name.value; | ||
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 or @include 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; | ||
} | ||
|
||
return evaledValue; | ||
} | ||
|
||
export function shouldInclude(selection: Selection, variables?: { [name: string]: any }): Boolean { | ||
if (!variables) { | ||
variables = {}; | ||
} | ||
|
||
if (!selection.directives) { | ||
return true; | ||
} | ||
|
||
let res: Boolean = true; | ||
selection.directives.map((directive) => { | ||
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. You don't need a map here. graphql-js assumes that there's only one skip or one include directive, so we can do the same. 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 think the spec actually defines the behavior that should occur when we have both a skip and include directive here. Since it does, I think it makes sense to include handling that case. 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 meant that there won't be two skips or two includes, at most one of each. |
||
if (directive.name.value !== 'skip' && directive.name.value !== 'include') { | ||
throw new Error(`Directive ${directive.name.value} not supported.`); | ||
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 think this should just ignore any directives that are not |
||
} | ||
if (!evaluateSkipInclude(directive, variables)) { | ||
res = false; | ||
} | ||
}); | ||
return res; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -72,21 +72,29 @@ export function createApolloStore({ | |
reduxRootKey = 'apollo', | ||
initialState, | ||
config = {}, | ||
reportCrashes, | ||
}: { | ||
reduxRootKey?: string, | ||
initialState?: any, | ||
config?: ApolloReducerConfig, | ||
reportCrashes?: boolean, | ||
} = {}): ApolloStore { | ||
const enhancers = []; | ||
|
||
if (reportCrashes === undefined) { | ||
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 had to add this because otherwise one of the unit tests would print all of this useless stuff every time it ran because it irked the crash reporter. 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 not sure I understand, can you show me later? 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. Use the TypeScript default argument syntax, |
||
reportCrashes = true; | ||
} | ||
|
||
if (typeof window !== 'undefined') { | ||
const anyWindow = window as any; | ||
if (anyWindow.devToolsExtension) { | ||
enhancers.push(anyWindow.devToolsExtension()); | ||
} | ||
} | ||
|
||
enhancers.push(applyMiddleware(crashReporter)); | ||
if (reportCrashes) { | ||
enhancers.push(applyMiddleware(crashReporter)); | ||
} | ||
|
||
return createStore( | ||
combineReducers({ [reduxRootKey]: createApolloReducer(config) }), | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -26,6 +26,14 @@ import { | |
applyMiddleware, | ||
} from 'redux'; | ||
|
||
import { | ||
createApolloStore, | ||
} from '../src/store'; | ||
|
||
import { | ||
QueryManager, | ||
} from '../src/QueryManager'; | ||
|
||
import { | ||
createNetworkInterface, | ||
HTTPNetworkInterface, | ||
|
@@ -707,6 +715,89 @@ describe('client', () => { | |
}); | ||
}); | ||
|
||
describe('directives', () => { | ||
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 think we can move these tests to |
||
it('should be able to send a query with a skip directive true', (done) => { | ||
const query = gql` | ||
query { | ||
fortuneCookie @skip(if: true) | ||
}`; | ||
const result = {}; | ||
const networkInterface = mockNetworkInterface( | ||
{ | ||
request: { query }, | ||
result: { data: result }, | ||
} | ||
); | ||
const client = new ApolloClient({ | ||
networkInterface, | ||
}); | ||
client.query({ query }).then((actualResult) => { | ||
assert.deepEqual(actualResult.data, result); | ||
done(); | ||
}); | ||
}); | ||
|
||
it('should be able to send a query with a skip directive false', (done) => { | ||
const query = gql` | ||
query { | ||
fortuneCookie @skip(if: false) | ||
}`; | ||
const result = { | ||
fortuneCookie: 'result', | ||
}; | ||
const networkInterface = mockNetworkInterface( | ||
{ | ||
request: { query }, | ||
result: { data: result }, | ||
} | ||
); | ||
const client = new ApolloClient({ | ||
networkInterface, | ||
}); | ||
client.query({ query }).then((actualResult) => { | ||
assert.deepEqual(actualResult.data, result); | ||
done(); | ||
}); | ||
}); | ||
|
||
it('should reject the query promise if skipped data arrives in the result', (done) => { | ||
const query = gql` | ||
query { | ||
fortuneCookie @skip(if: true) | ||
otherThing | ||
}`; | ||
const result = { | ||
fortuneCookie: 'you will go far', | ||
otherThing: 'false', | ||
}; | ||
const networkInterface = mockNetworkInterface( | ||
{ | ||
request: { query }, | ||
result: { data: result }, | ||
} | ||
); | ||
const client = new ApolloClient({ | ||
networkInterface, | ||
}); | ||
// we need this so it doesn't print out a bunch of stuff we don't need | ||
// when we're trying to test an exception. | ||
client.store = createApolloStore({ reportCrashes: false }); | ||
client.queryManager = new QueryManager({ | ||
networkInterface, | ||
store: client.store, | ||
reduxRootKey: 'apollo', | ||
}); | ||
|
||
client.query({ query }).then(() => { | ||
// do nothing | ||
}).catch((error) => { | ||
assert.include(error.message, 'Found extra field'); | ||
done(); | ||
}); | ||
|
||
}); | ||
}); | ||
|
||
describe('accepts dataIdFromObject option', () => { | ||
const query = gql` | ||
query people { | ||
|
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.
move this definition down to just before it is used.
and call it something other than
included
, maybeincludeField