Skip to content

Commit

Permalink
feat: implement argument stripping for @connection directive
Browse files Browse the repository at this point in the history
  • Loading branch information
bennyhobart committed Mar 1, 2018
1 parent 769625c commit 8f0eaa2
Show file tree
Hide file tree
Showing 3 changed files with 209 additions and 3 deletions.
51 changes: 49 additions & 2 deletions src/ParsedQueryNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import { CacheContext } from './context';
import { ConflictingFieldsError } from './errors';
import { DeepReadonly, JsonScalar, JsonObject, JsonValue, NestedObject, NestedValue } from './primitive';
import {
FieldNode,
ArgumentNode,
FragmentMap,
SelectionNode,
SelectionSetNode,
DirectiveNode,
ValueNode,
isObject,
fieldHasStaticDirective,
Expand Down Expand Up @@ -130,7 +132,7 @@ function _buildNodeMap(
// the schema. E.g. parameters are ignored, and an alias is considered
// to be truth.
if (!fieldHasStaticDirective(selection)) {
args = _buildFieldArgs(variables, selection.arguments);
args = _buildFieldArgs(variables, selection);
schemaName = selection.alias ? selection.name.value : undefined;
}

Expand Down Expand Up @@ -179,8 +181,16 @@ export function areChildrenDynamic(children?: ParsedQueryWithVariables) {
/**
* Build the map of arguments to their natural JS values (or variables).
*/
function _buildFieldArgs(variables: Set<string>, argumentsNode?: ArgumentNode[]) {
function _buildFieldArgs(variables: Set<string>, selection: FieldNode) {
const argumentsNode = selection.arguments;
if (!argumentsNode) return undefined;
if (!selection) return undefined;
if (selection.directives) {
const foundConnectionDirective = selection.directives.find(x => x.name.value === 'connection');
if (foundConnectionDirective) {
return _buildFieldArgsForConnectionDirective(variables, selection, foundConnectionDirective);
}
}

const args = {};
for (const arg of argumentsNode) {
Expand All @@ -191,6 +201,43 @@ function _buildFieldArgs(variables: Set<string>, argumentsNode?: ArgumentNode[])
return Object.keys(args).length ? args : undefined;
}

/**
* Applies the connection directive as described on (https://www.apollographql.com/docs/react/recipes/pagination.html#connection-directive)
*/
function _buildFieldArgsForConnectionDirective(variables: Set<string>, selection: FieldNode, connectionDirective: DirectiveNode) {
const argumentsNode = selection.arguments;
if (!argumentsNode) {
return undefined;
}
if (!connectionDirective.arguments) {
throw new Error('the connection directive requires arguments');
}
return connectionDirective.arguments.reduce((acc: Object, directive: ArgumentNode) => {
const name = directive.name.value;
const value = _valueFromNode(variables, directive.value);
if (name === 'key') {
if (typeof value !== 'string') {
throw new Error('the connection directive only supports keys which are strings');
}
return {
...acc,
key: value,
};
}
if (name === 'filter') {
if (!(value instanceof Array)) {
throw new Error('the connection directive only supports a list of keys');
}
const filterArgs = argumentsNode.filter(arg => value.some(v => v === arg.name.value));
return filterArgs.reduce((args, filterArg) => ({
...args,
[filterArg.name.value]: _valueFromNode(variables, filterArg.value),
}), acc);
}
throw new Error('Connection directive requires arguments of either key or filter');
}, {});
}

/**
* Evaluate a ValueNode and yield its value in its natural JS form.
*/
Expand Down
3 changes: 2 additions & 1 deletion src/util/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ export {
SelectionNode,
SelectionSetNode,
ValueNode,
// FieldNode,
FieldNode,
DirectiveNode,
} from 'graphql';

/**
Expand Down
158 changes: 158 additions & 0 deletions test/unit/ParsedQueryNode/directives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,162 @@ describe(`parseQuery with queries with directives`, () => {
);
});

describe('the connection directive', () => {
it(`respects the connection directive`, () => {
const operation1 = `
query getThings {
viewer {
friends(first: 50, after: "id") @connection(key: "friends") {
name
email
mobile
}
}
}
`;
const operation2 = `
query getThings {
viewer {
friends(first: 50, after: "id3") @connection(key: "friends") {
name
email
mobile
}
}
}
`;
expect(parseOperation(operation1)).to.deep.eq(parseOperation(operation2));
});

it(`caches by the key argument`, () => {
const operation1 = `
query FriendsQuery {
viewer {
friends(first: 50, after: "id") @connection(key: "friends") {
name
email
mobile
}
}
}
`;
const operation2 = `
query FriendsQuery {
viewer {
friends(first: 50, after: "id3") @connection(key: "friend") {
name
email
mobile
}
}
}
`;
expect(parseOperation(operation1)).to.not.deep.eq(parseOperation(operation2));
});

it(`caches by the combination of key and filter`, () => {
const operation1 = `
query FriendsQuery {
viewer {
friends(onlyCloseFriends: true, first: 1) @connection(key: "friends", filter: ["onlyCloseFriends"]) {
name
email
mobile
}
}
}
`;
const operation2 = `
query FriendsQuery {
viewer {
friends(onlyCloseFriends: true, first: 2) @connection(key: "friends", filter: ["onlyCloseFriends"]) {
name
email
mobile
}
}
}
`;
const operation3 = `
query FriendsQuery {
viewer {
friends(onlyCloseFriends: false, first: 1) @connection(key: "friends", filter: ["onlyCloseFriends"]) {
name
email
mobile
}
}
}
`;
expect(parseOperation(operation1)).to.deep.eq(parseOperation(operation2));
expect(parseOperation(operation1)).to.not.deep.eq(parseOperation(operation3));
});

it('caches based on multiple filters', () => {
const operation1 = `
query FriendsQuery {
viewer {
friends(
onlyCloseFriends: true,
attractive: true,
first: 1
) @connection(key: "friends", filter: ["onlyCloseFriends", "attractive"]) {
name
email
mobile
}
}
}
`;
const operation2 = `
query FriendsQuery {
viewer {
friends(
onlyCloseFriends: true,
attractive: true,
first: 2
) @connection(key: "friends", filter: ["onlyCloseFriends", "attractive"]) {
name
email
mobile
}
}
}
`;
const operation3 = `
query FriendsQuery {
viewer {
friends(
onlyCloseFriends: false,
attractive: true,
first: 1
) @connection(key: "friends", filter: ["onlyCloseFriends", "attractive"]) {
name
email
mobile
}
}
}
`;
const operation4 = `
query FriendsQuery {
viewer {
friends(
onlyCloseFriends: false,
attractive: false,
first: 1
) @connection(key: "friends", filter: ["onlyCloseFriends", "attractive"]) {
name
email
mobile
}
}
}
`;
expect(parseOperation(operation1)).to.deep.eq(parseOperation(operation2));
expect(parseOperation(operation1)).to.not.deep.eq(parseOperation(operation3));
expect(parseOperation(operation1)).to.not.deep.eq(parseOperation(operation4));
expect(parseOperation(operation3)).to.not.deep.eq(parseOperation(operation4));
});
});
});

0 comments on commit 8f0eaa2

Please sign in to comment.