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

allow rangeBehaviors to be defined as a function #1054

Closed
Closed
Show file tree
Hide file tree
Changes from all 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
31 changes: 29 additions & 2 deletions docs/Guides-Mutations.md
Original file line number Diff line number Diff line change
Expand Up @@ -329,9 +329,36 @@ Given a parent, a connection, and the name of the newly created edge in the resp

The field name in the response that represents the newly created edge

- `rangeBehaviors: {[call: string]: GraphQLMutatorConstants.RANGE_OPERATIONS}`
- `rangeBehaviors: {[call: string]: GraphQLMutatorConstants.RANGE_OPERATIONS} | (connectionArgs: {[argName: string]: string}) => $Enum<GraphQLMutatorConstants.RANGE_OPERATIONS>`

A map between printed, dot-separated GraphQL calls *in alphabetical order*, and the behavior we want Relay to exhibit when adding the new edge to connections under the influence of those calls. Behaviors can be one of `'append'`, `'ignore'`, `'prepend'`, `'refetch'`, or `'remove'`.
A map between printed, dot-separated GraphQL calls *in alphabetical order* and the behavior we want Relay to exhibit when adding the new edge to connections under the influence of those calls or a function accepting an array of connection arguments, returning that behavior.

For example, `rangeBehaviors` could be written this way:

```
const rangeBehaviors = {
// When the ships connection is not under the influence
// of any call, append the ship to the end of the connection
'': 'append',
// Prepend the ship, wherever the connection is sorted by age
'orderby(newest)': 'prepend',
};
```

Or this way, with the same results:

```
const rangeBehaviors = ({orderby}) => {
if (orderby === 'newest') {
return 'prepend';
} else {
return 'append';
}
};

```

Behaviors can be one of `'append'`, `'ignore'`, `'prepend'`, `'refetch'`, or `'remove'`.

#### Example

Expand Down
11 changes: 6 additions & 5 deletions examples/todo/js/mutations/AddTodoMutation.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,12 @@ export default class AddTodoMutation extends Relay.Mutation {
parentID: this.props.viewer.id,
connectionName: 'todos',
edgeName: 'todoEdge',
rangeBehaviors: {
'': 'append',
'status(any)': 'append',
'status(active)': 'append',
'status(completed)': 'ignore',
rangeBehaviors: ({status}) => {
if (status === 'completed') {
return 'ignore';
} else {
return 'append';
}
},
}];
}
Expand Down
17 changes: 13 additions & 4 deletions src/mutation/RelayMutationQuery.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const {REFETCH} = require('GraphQLMutatorConstants');

const flattenRelayQuery = require('flattenRelayQuery');
const forEachObject = require('forEachObject');
const getRangeBehavior = require('getRangeBehavior');
const nullthrows = require('nullthrows');
const inferRelayFieldsFromData = require('inferRelayFieldsFromData');
const intersectRelayQuery = require('intersectRelayQuery');
Expand Down Expand Up @@ -222,9 +223,11 @@ const RelayMutationQuery = {
return;
}

const rangeBehaviorKey = trackedConnection.getRangeBehaviorKey();
const rangeBehaviorValue = rangeBehaviors[rangeBehaviorKey];
if (rangeBehaviorKey in rangeBehaviors && rangeBehaviorValue !== REFETCH) {
const callsWithValues = trackedConnection.getRangeBehaviorCalls();
const rangeBehavior =
getRangeBehavior(rangeBehaviors, callsWithValues);

if (rangeBehavior && rangeBehavior !== REFETCH) {
// Include edges from all connections that exist in `rangeBehaviors`.
// This may add duplicates, but they will eventually be flattened.
trackedEdges.forEach(trackedEdge => {
Expand All @@ -234,7 +237,7 @@ const RelayMutationQuery = {
// If the connection is not in `rangeBehaviors` or we have explicitly
// set the behavior to `refetch`, re-fetch it.
warning(
rangeBehaviorValue === REFETCH,
rangeBehavior === REFETCH,
'RelayMutation: The connection `%s` on the mutation field `%s` ' +
'that corresponds to the ID `%s` did not match any of the ' +
'`rangeBehaviors` specified in your RANGE_ADD config. This means ' +
Expand Down Expand Up @@ -533,6 +536,12 @@ function sanitizeRangeBehaviors(
// Prior to 0.4.1 you would have to specify the args in your range behaviors
// in the same order they appeared in your query. From 0.4.1 onward, args in a
// range behavior key must be in alphabetical order.

// No need to sanitize if defined as a function
if (typeof rangeBehaviors === 'function') {
return rangeBehaviors;
}

let unsortedKeys;
forEachObject(rangeBehaviors, (value, key) => {
if (key !== '') {
Expand Down
54 changes: 54 additions & 0 deletions src/mutation/__tests__/RelayMutationQuery-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ require('configureForRelayOSS');

jest
.dontMock('RelayMutationQuery')
.dontMock('getRangeBehavior')
.mock('warning');

const GraphQLMutatorConstants = require('GraphQLMutatorConstants');
Expand Down Expand Up @@ -515,6 +516,59 @@ describe('RelayMutationQuery', () => {
]);
});

it('includes edge fields for connections with rangeBehaviors function', () => {
tracker.getTrackedChildrenForID.mockReturnValue(getNodeChildren(Relay.QL`
fragment on Feedback {
comments(orderby:"toplevel",first:"10") {
edges {
node {
body {
text
}
}
}
}
}
`));
const node = RelayMutationQuery.buildFragmentForEdgeInsertion({
fatQuery,
tracker,
connectionName: 'comments',
parentID: '123',
edgeName: 'feedbackCommentEdge',
rangeBehaviors: ({orderby}) => {
if (orderby === 'toplevel') {
return 'append';
} else {
return 'refetch';
}
},
});
const expected = getNodeWithoutSource(Relay.QL`
fragment on CommentCreateResponsePayload {
feedbackCommentEdge {
__typename
cursor,
node {
body {
text
},
id
},
source {
id
}
}
}
`);
expect(node)
.toEqualQueryNode(expected);
expect(tracker.getTrackedChildrenForID.mock.calls).toEqual([
['123'],
]);
});


it('includes fields from multiple tracked edges', () => {
tracker.getTrackedChildrenForID.mockReturnValue(getNodeChildren(Relay.QL`
fragment on Feedback {
Expand Down
54 changes: 54 additions & 0 deletions src/mutation/__tests__/getRangeBehavior-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* Copyright (c) 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @emails oncall+relay
*/

'use strict';

jest.dontMock('getRangeBehavior');
const RelayTestUtils = require('RelayTestUtils');
const getRangeBehavior = require('getRangeBehavior');

describe('getRangeBehavior()', () => {
describe('when rangeBehaviors are defined as a function', () => {
const rangeBehaviors = ({status}) => {
if (status === 'any') {
return 'append';
} else {
return 'refetch';
}
}

it('returns the rangeBehavior to use with the connectionArgs', () => {
const calls = [{name: 'status', value: 'any'}];
const rangeBehavior = getRangeBehavior(rangeBehaviors, calls);
expect(rangeBehavior).toBe('append');
});
});

describe('when rangeBehaviors are a plain object', () => {
const rangeBehaviors = {
'status(any)': 'append',
'': 'prepend',
};

it('returns the rangeBehavior associated with the rangeBehaviorKey', () => {
const calls = [{name: 'status', value: 'any'}];
const rangeBehavior = getRangeBehavior(rangeBehaviors, calls);
expect(rangeBehavior).toBe('append');
});

it('returns null when no rangeBehavior is associated with the rangeBehaviorKey', () => {
const calls = [{name: 'status', value: 'recent'}];
const rangeBehavior = getRangeBehavior(rangeBehaviors, calls);
expect(rangeBehavior).toBe(null);
});
});
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: it would be good to test a non-matching case too

});

66 changes: 66 additions & 0 deletions src/mutation/getRangeBehavior.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* Copyright (c) 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule getRangeBehavior
* @typechecks
* @flow
*/

'use strict';

import type {
Call,
RangeBehaviors,
} from 'RelayInternalTypes';

const serializeRelayQueryCall = require('serializeRelayQueryCall');

/**
* Return the action (prepend/append) to use when adding an item to
* the range with the specified calls.
*
* Ex:
* rangeBehaviors: `{'orderby(recent)': 'append'}`
* calls: `[{name: 'orderby', value: 'recent'}]`
*
* Returns `'append'`
*/
function getRangeBehavior(
rangeBehaviors: RangeBehaviors,
calls: Array<Call>
): ?string {
if (typeof rangeBehaviors === 'function') {
const rangeFilterCalls = getObjectFromCalls(calls);
return rangeBehaviors(rangeFilterCalls);
} else {
const rangeBehaviorKey =
calls.map(serializeRelayQueryCall).sort().join('').slice(1);
return rangeBehaviors[rangeBehaviorKey] || null;
}
}

/**
* Returns an object representation of the rangeFilterCalls that
* will be passed to config.rangeBehaviors
*
* Example:
* calls: `[{name: 'orderby', value: 'recent'}]`
*
* Returns:
* `{orderby: 'recent'}`
*/
function getObjectFromCalls(
calls: Array<Call>
): {[argName: string]: string} {
return calls.reduce((rangeFilterCalls, call) => {
rangeFilterCalls[call.name] = call.value;
return rangeFilterCalls;
},{})
}

module.exports = getRangeBehavior;
42 changes: 19 additions & 23 deletions src/query/RelayQuery.js
Original file line number Diff line number Diff line change
Expand Up @@ -947,7 +947,7 @@ class RelayQueryFragment extends RelayQueryNode {
class RelayQueryField extends RelayQueryNode {
__debugName__: ?string;
__isRefQueryDependency__: boolean;
__rangeBehaviorKey__: ?string;
__rangeBehaviorCalls__: ?Array<Call>;
__shallowHash__: ?string;

static create(
Expand Down Expand Up @@ -1015,7 +1015,7 @@ class RelayQueryField extends RelayQueryNode {
super(concreteNode, route, variables);
this.__debugName__ = undefined;
this.__isRefQueryDependency__ = false;
this.__rangeBehaviorKey__ = undefined;
this.__rangeBehaviorCalls__ = undefined;
this.__shallowHash__ = undefined;
}

Expand Down Expand Up @@ -1085,34 +1085,30 @@ class RelayQueryField extends RelayQueryNode {
}

/**
* A string representing the range behavior eligible arguments associated with
* this field. Arguments will be sorted.
*
* Non-core arguments (like connection and identifying arguments) are dropped.
* `field(first: 10, foo: "bar", baz: "bat")` => `'baz(bat).foo(bar)'`
* `username(name: "steve")` => `''`
*/
getRangeBehaviorKey(): string {
* An Array of Calls to be used with rangeBehavior config functions.
*
* Non-core arguments (like connection and identifying arguments) are dropped.
* `field(first: 10, foo: "bar", baz: "bat")` => `'baz(bat).foo(bar)'`
* `username(name: "steve")` => `''`
*/
getRangeBehaviorCalls(): Array<Call> {
invariant(
this.isConnection(),
'RelayQueryField: Range behavior keys are associated exclusively with ' +
'connection fields. `getRangeBehaviorKey()` was called on the ' +
'connection fields. `getRangeBehaviorCalls()` was called on the ' +
'non-connection field `%s`.',
this.getSchemaName()
);
let rangeBehaviorKey = this.__rangeBehaviorKey__;
if (rangeBehaviorKey == null) {
const printedCoreArgs = [];
this.getCallsWithValues().forEach(arg => {
if (this._isCoreArg(arg)) {
printedCoreArgs.push(serializeRelayQueryCall(arg));
}
});
rangeBehaviorKey = printedCoreArgs.sort().join('').slice(1);
this.__rangeBehaviorKey__ = rangeBehaviorKey;

let rangeBehaviorCalls = this.__rangeBehaviorCalls__;
if (!rangeBehaviorCalls) {
rangeBehaviorCalls = this.getCallsWithValues().filter(arg => {
return this._isCoreArg(arg);
});
this.__rangeBehaviorCalls__ = rangeBehaviorCalls;
}
return rangeBehaviorKey;
}
return rangeBehaviorCalls;
}

/**
* The name for the field when serializing the query or interpreting query
Expand Down
Loading