/**
* Copyright 2013-2015, 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 GraphQLRange
* @typechecks
*/
'use strict';
var _classCallCheck = require('babel-runtime/helpers/class-call-check')['default'];
var _defineProperty = require('babel-runtime/helpers/define-property')['default'];
var _extends = require('babel-runtime/helpers/extends')['default'];
var _slicedToArray = require('babel-runtime/helpers/sliced-to-array')['default'];
var _toConsumableArray = require('babel-runtime/helpers/to-consumable-array')['default'];
var GraphQLMutatorConstants = require('./GraphQLMutatorConstants');
var GraphQLSegment = require('./GraphQLSegment');
var GraphQLStoreDataHandler = require('./GraphQLStoreDataHandler');
var RelayConnectionInterface = require('./RelayConnectionInterface');
var forEachObject = require('fbjs/lib/forEachObject');
var invariant = require('fbjs/lib/invariant');
var serializeRelayQueryCall = require('./serializeRelayQueryCall');
var warning = require('fbjs/lib/warning');
var END_CURSOR = RelayConnectionInterface.END_CURSOR;
var HAS_NEXT_PAGE = RelayConnectionInterface.HAS_NEXT_PAGE;
var HAS_PREV_PAGE = RelayConnectionInterface.HAS_PREV_PAGE;
var START_CURSOR = RelayConnectionInterface.START_CURSOR;
/**
* @param {array<object>} queryCalls
* @return {object}
*/
function callsArrayToObject(queryCalls) {
var calls = {};
for (var ii = 0; ii < queryCalls.length; ii++) {
if (RelayConnectionInterface.isConnectionCall(queryCalls[ii])) {
var _queryCalls$ii = queryCalls[ii];
var name = _queryCalls$ii.name;
var value = _queryCalls$ii.value;
// assuming that range calls will only have a single argument
if (Array.isArray(value) && value.length) {
value = value[0];
}
// Ignore the whole call when the value is null
if (value === null) {
continue;
}
calls[name] = value;
}
}
// update first and last call values to be numbers
if (calls.first) {
!!isNaN(calls.first) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'GraphQLRange: Expected `first` argument to be a number, got ' + '`%s`.', calls.first) : invariant(false) : undefined;
calls.first = +calls.first;
} else if (calls.last) {
!!isNaN(calls.last) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'GraphQLRange: Expected `last` argument to be a number, got ' + '`%s`.', calls.last) : invariant(false) : undefined;
calls.last = +calls.last;
}
return calls;
}
/**
* Returns whether this is currently a set of static calls that GraphQLRange
* supports. Static calls define ranges that do not change over a period
* of time, given the same set of arguments.
*
* @param {object} calls
* @return {?boolean}
*/
function isStaticCall(calls) {
return calls.hasOwnProperty('surrounds') || calls.hasOwnProperty('find');
}
/**
* Returns whether this is currently a set of calls that GraphQLRange
* supports
*
* @param {object} calls
* @return {boolean}
*/
function isValidRangeCall(calls) {
var hasFirst = calls.hasOwnProperty('first');
var hasLast = calls.hasOwnProperty('last');
// Currently only supports: first(), after().first(), last(), before().last()
// before().first(), after().last(), after().before().first(), and
// after().before().last()
// first() can never be called with last().
return (hasFirst || hasLast) && !(hasFirst && hasLast);
}
/**
* Returns whether the call values are supported by GraphQLRange
*
* @param {object} calls
* @return {boolean}
*/
function isValidRangeCallValues(calls) {
return calls.hasOwnProperty('first') && calls.first > 0 || calls.hasOwnProperty('last') && calls.last > 0;
}
/**
* Validates edge to ensure it has all the fields needed to be store properly.
*
* @param {object} edge
*/
function validateEdge(edge) {
!(GraphQLStoreDataHandler.getID(edge) !== undefined) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'GraphQLStore: `edge` must have a data id') : invariant(false) : undefined;
!(edge.node !== undefined) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'GraphQLStore: `edge` must have `node` field') : invariant(false) : undefined;
}
/**
* @param {array<object>} edges
*/
function validateEdges(edges) {
edges.forEach(validateEdge);
}
/**
* A range represents an ordered set of edges. Methods are provided for adding
* edges (`appendEdge`, `prependEdge`, `addItems`) and removing them
* (`removeEdgeWithID`).
*
* Within a range, each contiguous group of edges is modeled using a
* `GraphQLSegment`, but this is an implementation detail that `GraphQLRange`
* hides from its callers.
*
* Ranges model GraphQL connections, which are the means of traversing from a
* node to a set of associated objects; for example, in the following query the
* "friends" connection produces a range containing edges that lead to the
* requested friend nodes:
*
* node(4) {
* friends.first(2) {
* edges {
* node {
* id,
* name,
* },
* },
* },
* }
*
* @see `GraphQLSegment`
* @see "Connections" in https://fburl.com/graphql-connections
* @internal
*/
var GraphQLRange = (function () {
function GraphQLRange() {
_classCallCheck(this, GraphQLRange);
this.reset();
}
/**
* @param {array<object>} calls
* @return {string}
*/
GraphQLRange.prototype.reset = function reset() {
// List of segments where each segment is a continuous chunk.
// There are gaps in between the segments. The first segment in the list
// should be cursors beginning at the top of the range (i.e. first(N)).
// The last segment in the list should be cursors at the bottom of
// the range (i.e. last(N)).
this._orderedSegments = [new GraphQLSegment(), new GraphQLSegment()];
// GraphQLRange nodes can also support static queries like surrounds,
// find, whose contents won't ever change for a given set of arguments.
// Store these queries' results in this map, since you can't do first()
// or last() queries on these ranges.
this._staticQueriesMap = {};
this._hasFirst = false;
this._hasLast = false;
};
/**
* @param {number} index
*/
GraphQLRange.prototype._resetSegment = function _resetSegment(index) {
!(index >= 0 && index < this._orderedSegments.length) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'cannot reset non-existent segment') : invariant(false) : undefined;
this._orderedSegments[index] = new GraphQLSegment();
};
/**
* @param {string} cursor
* @return {?number}
*/
GraphQLRange.prototype._getSegmentIndexByCursor = function _getSegmentIndexByCursor(cursor) {
// TODO: revisit if we end up having too many segments
for (var ii = 0; ii < this._orderedSegments.length; ii++) {
if (this._orderedSegments[ii].containsEdgeWithCursor(cursor)) {
return ii;
}
}
return null;
};
/**
* @param {string} id
* @return {?number}
*/
GraphQLRange.prototype._getSegmentIndexByID = function _getSegmentIndexByID(id) {
// TODO: revisit if we end up having too many segments
for (var ii = 0; ii < this._orderedSegments.length; ii++) {
if (this._orderedSegments[ii].containsEdgeWithID(id)) {
return ii;
}
}
return null;
};
/**
* Add edges' data into the static queries map for the query calls,
* overwriting any previously existing data for these calls.
* @param {array<object>} queryCalls
* @param {array} edges
*/
GraphQLRange.prototype._addStaticEdges = function _addStaticEdges(queryCalls, edges) {
var calls = _callsToString(queryCalls);
var edgeIDsToStore = [];
var cursorsToStore = [];
for (var ii = 0; ii < edges.length; ii++) {
var edge = edges[ii];
edgeIDsToStore.push(GraphQLStoreDataHandler.getID(edge));
cursorsToStore.push(edge.cursor);
}
this._staticQueriesMap[calls] = {
edgeIDs: edgeIDsToStore,
cursors: cursorsToStore
};
};
/**
* Add edges into the range based on the query calls. New edges will replace
* previous edges in the range.
* @param {array<object>} queryCalls
* @param {array} edges
* @param {object} pageInfo
*/
GraphQLRange.prototype.addItems = function addItems(queryCalls, edges, pageInfo) {
validateEdges(edges);
var calls = callsArrayToObject(queryCalls);
var segmentCount, segmentIndex;
if (isStaticCall(calls)) {
this._addStaticEdges(queryCalls, edges);
return;
}
if (!isValidRangeCall(calls)) {
console.error('GraphQLRange currently only handles first(<count>), ' + 'after(<cursor>).first(<count>), last(<count>), ' + 'before(<cursor>).last(<count>), before(<cursor>).first(<count>), ' + 'and after(<cursor>).last(<count>)');
return;
}
// Skip the update if cursors are invalid
if (calls.before === null || calls.after === null) {
console.error('GraphQLRange received null as a cursor.');
return;
}
if (calls.first) {
// before().first() calls can produce gaps
if (calls.before && !calls.after) {
// make a new segment if there is a gap
if (pageInfo[HAS_NEXT_PAGE] === true) {
if (this._getSegmentIndexByCursor(calls.before) === 0) {
this._orderedSegments.unshift(new GraphQLSegment());
}
// When there is a gap from before().first() query, this is the same
// as just storing a first().
this._addAfterFirstItems(edges, pageInfo[HAS_NEXT_PAGE], undefined, calls.before);
} else {
// Since there is no gap, we can stitch into the beginning
// of existing segment
this._addBeforeLastItems(edges, pageInfo[HAS_PREV_PAGE], calls.before);
}
} else {
// These elements are added from paging to extend the the range.
if (!calls.after) {
segmentIndex = 0;
segmentCount = this.getFirstSegment().getCount();
if (segmentCount && (calls.first > segmentCount || edges.length > segmentCount) && !this.getFirstSegment().getFirstCursor()) {
// this is a range for which we don't have a cursor, and we've
// fetched more data by increasing the `first(N)` variable; we
// blow away and replace the first segment in order to side-step
// issues where the order of IDs in the range may change between
// queries
this._resetSegment(segmentIndex);
}
}
this._addAfterFirstItems(edges, pageInfo[HAS_NEXT_PAGE], calls.after, calls.before);
}
} else if (calls.last) {
// after().last() calls can produce gaps
if (calls.after && !calls.before) {
// make a new segment if there is a gap
if (pageInfo[HAS_PREV_PAGE] === true) {
if (this._getSegmentIndexByCursor(calls.after) === this._orderedSegments.length - 1) {
this._orderedSegments.push(new GraphQLSegment());
}
// When there is a gap from after().last() query, this is the same as
// just storing a last().
this._addBeforeLastItems(edges, pageInfo[HAS_PREV_PAGE], undefined, calls.after);
} else {
// Since there is no gap, we can stitch to the end
// of existing segment
this._addAfterFirstItems(edges, pageInfo[HAS_NEXT_PAGE], calls.after);
}
} else {
// These elements are added from paging to extend the the range.
if (!calls.before) {
segmentIndex = this._orderedSegments.length - 1;
segmentCount = this.getLastSegment().getCount();
if (segmentCount && (calls.last > segmentCount || edges.length > segmentCount) && !this.getLastSegment().getLastCursor()) {
// this is a range for which we don't have a cursor, and we've
// fetched more data by increasing the `last(N)` variable; we
// blow away and replace the last segment in order to side-step
// issues where the order of IDs in the range may change between
// queries
this._resetSegment(segmentIndex);
}
}
this._addBeforeLastItems(edges, pageInfo[HAS_PREV_PAGE], calls.before, calls.after);
}
}
};
/**
* @return {GraphQLSegment}
*/
GraphQLRange.prototype.getFirstSegment = function getFirstSegment() {
return this._orderedSegments[0];
};
/**
* @return {GraphQLSegment}
*/
GraphQLRange.prototype.getLastSegment = function getLastSegment() {
return this._orderedSegments[this._orderedSegments.length - 1];
};
/**
* Tries to concat segments at segmentIndex and segmentIndex + 1.
* This is an all or nothing operation.
* If concat is successful, we'll remove the segment at segmentIndex + 1
* from the orderedSegments after all elements has been added to the segment
* at segmentIndex.
* If concat is unsuccessful, nothing will be changed.
* @param {number} segmentIndex
*/
GraphQLRange.prototype._concatSegments = function _concatSegments(segmentIndex) {
!(segmentIndex + 1 < this._orderedSegments.length && segmentIndex >= 0) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'GraphQLRange cannot concat segments outside the range ' + 'of orderedSegments') : invariant(false) : undefined;
var firstSegment = this._orderedSegments[segmentIndex];
var secondSegment = this._orderedSegments[segmentIndex + 1];
if (firstSegment.concatSegment(secondSegment)) {
this._orderedSegments.splice(segmentIndex + 1, 1);
} else {
console.warn('GraphQLRange was unable to concat segment %d and segment %d', segmentIndex, segmentIndex + 1);
}
};
/**
* Adds the edge to the front of the range. New edge will replace previous
* edge that have the same id.
* @param {object} edge
*/
GraphQLRange.prototype.prependEdge = function prependEdge(edge) {
validateEdge(edge);
this._hasFirst = true;
this._removeEdgeIfApplicable(edge);
var segment = this.getFirstSegment();
segment.prependEdge(edge);
};
/**
* Adds the edge to the end of the range. New edge will replace previous
* edge that have the same id.
* @param {object} edge
*/
GraphQLRange.prototype.appendEdge = function appendEdge(edge) {
validateEdge(edge);
this._hasLast = true;
this._removeEdgeIfApplicable(edge);
var segment = this.getLastSegment();
segment.appendEdge(edge);
};
/**
* Removes edge in range if it matches id in input edge.
* @param {object} edge
*/
GraphQLRange.prototype._removeEdgeIfApplicable = function _removeEdgeIfApplicable(edge) {
var id = GraphQLStoreDataHandler.getID(edge);
var index = this._getSegmentIndexByID(id);
if (index != null) {
this._orderedSegments[index].removeEdge(id);
}
};
/**
* Remove any edges in the range if it matches any of the ids in the input.
* This function is used to prevent us from adding any id that already exist
* in the range.
*
* @param {array} edges
*/
GraphQLRange.prototype._removeEdgesIfApplicable = function _removeEdgesIfApplicable(edges) {
for (var ii = 0; ii < edges.length; ii++) {
this._removeEdgeIfApplicable(edges[ii]);
}
};
/**
* Add items into the correct segment with the cursor. If no cursor
* is present, items are added to the very first segment.
*
* @param {array} edges
* @param {boolean} hasNextPage
* @param {?string} afterCursor
* @param {?string} beforeCursor
*/
GraphQLRange.prototype._addAfterFirstItems = function _addAfterFirstItems(edges, hasNextPage, afterCursor, beforeCursor) {
var segment;
var segmentIndex;
var lastCursor;
if (afterCursor !== undefined) {
segmentIndex = this._getSegmentIndexByCursor(afterCursor);
if (segmentIndex == null) {
process.env.NODE_ENV !== 'production' ? warning(false, 'GraphQLRange cannot find a segment that has the cursor: %s', afterCursor) : undefined;
return;
}
segment = this._orderedSegments[segmentIndex];
lastCursor = segment.getLastCursor();
if (lastCursor !== afterCursor) {
edges = this._reconcileAfterFirstEdges(segment, edges, afterCursor);
afterCursor = lastCursor;
if (!edges) {
return;
}
}
} else {
segmentIndex = 0;
segment = this._orderedSegments[segmentIndex];
lastCursor = segment.getLastCursor();
if (lastCursor !== undefined) {
edges = this._reconcileAfterFirstEdges(segment, edges);
afterCursor = lastCursor;
if (!edges) {
return;
}
}
}
if (beforeCursor !== undefined) {
if (segmentIndex === this._orderedSegments.length - 1) {
console.warn('GraphQLRange cannot add because there is no next segment');
return;
} else if (this._orderedSegments[segmentIndex + 1].getFirstCursor() !== beforeCursor) {
process.env.NODE_ENV !== 'production' ? warning(false, 'GraphQLRange cannot add because beforeCursor does not match first ' + 'cursor of the next segment') : undefined;
return;
}
}
if (afterCursor === undefined) {
this._hasFirst = true;
}
this._removeEdgesIfApplicable(edges);
segment.addEdgesAfterCursor(edges, afterCursor);
if (!hasNextPage) {
if (beforeCursor !== undefined) {
// If we have a beforeCursor and there is no next page,
// then there is no gap between the current segment and the next.
// We can concat the two segments when there is no gap.
this._concatSegments(segmentIndex);
} else {
this._hasLast = true;
// If this segment already has the last element, we don't
// need any segments after this.
this._orderedSegments.splice(segmentIndex + 1, this._orderedSegments.length - 1 - segmentIndex);
}
}
};
/**
* In the case the cursor does not correspond last cursor,
* walk through the edges to see if we can trim edges to
* only those after the last cursor. Returns undefined when
* the input cannot be reconciled.
*
* @param {GraphQLSegment} segment
* @param {array} edges
* @param {?string} cursor
* @return {?array} trimmed edges
*/
GraphQLRange.prototype._reconcileAfterFirstEdges = function _reconcileAfterFirstEdges(segment, edges, cursor) {
var metadata = segment.getMetadataAfterCursor(edges.length + 1, cursor);
var edgeIDs = metadata.edgeIDs;
if (edgeIDs.length > edges.length) {
// Already have more edges than the input.
return undefined;
}
for (var ii = 0; ii < edgeIDs.length; ii++) {
if (edgeIDs[ii] !== GraphQLStoreDataHandler.getID(edges[ii])) {
process.env.NODE_ENV !== 'production' ? warning(false, 'Relay was unable to reconcile edges on a connection. This most ' + 'likely occurred while trying to handle a server response that ' + 'includes connection edges with nodes that lack an `id` field.') : undefined;
return undefined;
}
}
return edges.slice(edgeIDs.length);
};
/**
* Add items into the correct segment with the cursor. If no cursor
* is present, items are added to the very last segment.
* @param {array} edges
* @param {boolean} hasPrevPage
* @param {?string} beforeCursor
* @param {?string} afterCursor
*/
GraphQLRange.prototype._addBeforeLastItems = function _addBeforeLastItems(edges, hasPrevPage, beforeCursor, afterCursor) {
var segment;
var segmentIndex;
var firstCursor;
if (beforeCursor !== undefined) {
segmentIndex = this._getSegmentIndexByCursor(beforeCursor);
if (segmentIndex == null) {
process.env.NODE_ENV !== 'production' ? warning(false, 'GraphQLRange cannot find a segment that has the cursor: %s', beforeCursor) : undefined;
return;
}
segment = this._orderedSegments[segmentIndex];
firstCursor = segment.getFirstCursor();
if (firstCursor !== beforeCursor) {
edges = this._reconcileBeforeLastEdges(segment, edges, beforeCursor);
beforeCursor = firstCursor;
if (!edges) {
return;
}
}
} else {
segmentIndex = this._orderedSegments.length - 1;
segment = this._orderedSegments[segmentIndex];
firstCursor = segment.getFirstCursor();
if (firstCursor !== undefined) {
edges = this._reconcileBeforeLastEdges(segment, edges, beforeCursor);
beforeCursor = firstCursor;
if (!edges) {
return;
}
}
}
if (afterCursor !== undefined) {
if (segmentIndex === 0) {
console.warn('GraphQLRange cannot add because there is no previous segment');
return;
} else if (this._orderedSegments[segmentIndex - 1].getLastCursor() !== afterCursor) {
process.env.NODE_ENV !== 'production' ? warning(false, 'GraphQLRange cannot add because afterCursor does not match last ' + 'cursor of the previous segment') : undefined;
return;
}
}
if (beforeCursor === undefined) {
this._hasLast = true;
}
this._removeEdgesIfApplicable(edges);
segment.addEdgesBeforeCursor(edges, beforeCursor);
if (!hasPrevPage) {
if (afterCursor !== undefined) {
// If we have an afterCursor and there is no previous page,
// then there is no gap between the current segment and the previous.
// We can concat the two segments when there is no gap.
this._concatSegments(segmentIndex - 1);
} else {
this._hasFirst = true;
// If this segment already has the first element, we don't
// need any segments before this.
this._orderedSegments.splice(0, segmentIndex);
}
}
};
/**
* In the case the cursor does not correspond first cursor,
* walk through the edges to see if we can trim edges to
* only those before the first cursor. Returns undefined when
* the input cannot be reconciled.
*
* @param {GraphQLSegment} segment
* @param {array} edges
* @param {?string} cursor
* @return {?array} trimmed edges
*/
GraphQLRange.prototype._reconcileBeforeLastEdges = function _reconcileBeforeLastEdges(segment, edges, cursor) {
var metadata = segment.getMetadataBeforeCursor(edges.length + 1, cursor);
var edgeIDs = metadata.edgeIDs;
if (edgeIDs.length > edges.length) {
// Already have more edges than the input.
return undefined;
}
for (var ii = 1; ii <= edgeIDs.length; ii++) {
if (edgeIDs[edgeIDs.length - ii] !== GraphQLStoreDataHandler.getID(edges[edges.length - ii])) {
process.env.NODE_ENV !== 'production' ? warning(false, 'Relay was unable to reconcile edges on a connection. This most ' + 'likely occurred while trying to handle a server response that ' + 'includes connection edges with nodes that lack an `id` field.') : undefined;
return undefined;
}
}
return edges.slice(0, edges.length - edgeIDs.length);
};
/**
* Removes an edge from this range such that the edge will never be reachable
* regardless of the client session. This is used by delete mutations.
*
* @param {string} id
*/
GraphQLRange.prototype.removeEdgeWithID = function removeEdgeWithID(id) {
for (var ii = 0; ii < this._orderedSegments.length; ii++) {
this._orderedSegments[ii].removeAllEdges(id);
}
};
/**
* @param {array<object>} queryCalls
* @param {?object} optimisticData
* @return {object} includes fields: requestedEdgeIDs, diffCalls
*/
GraphQLRange.prototype.retrieveRangeInfoForQuery = function retrieveRangeInfoForQuery(queryCalls, optimisticData) {
var calls = callsArrayToObject(queryCalls);
if (isStaticCall(calls)) {
return this._retrieveRangeInfoForStaticCalls(queryCalls);
}
// Convert to name => true, so we can test for whether the key exists
// without comparing to undefined
if (!isValidRangeCall(calls)) {
console.error('GraphQLRange currently only handles first(<count>), ' + 'after(<cursor>).first(<count>), last(<count>), ' + 'before(<cursor>).last(<count>), before(<cursor>).first(<count>), ' + 'and after(<cursor>).last(<count>)');
return {
requestedEdgeIDs: [],
diffCalls: [],
pageInfo: RelayConnectionInterface.getDefaultPageInfo()
};
}
if (calls.first && calls.before || calls.last && calls.after) {
// TODO #7556678: add support for first/before and last/after
return {
requestedEdgeIDs: [],
diffCalls: [],
pageInfo: RelayConnectionInterface.getDefaultPageInfo()
};
}
if (!isValidRangeCallValues(calls)) {
console.error('GraphQLRange only supports first(<count>) or last(<count>) ' + 'where count is greater than 0');
return {
requestedEdgeIDs: [],
diffCalls: [],
pageInfo: RelayConnectionInterface.getDefaultPageInfo()
};
}
if (calls.first) {
return this._retrieveRangeInfoForFirstQuery(queryCalls, optimisticData);
} else if (calls.last) {
return this._retrieveRangeInfoForLastQuery(queryCalls, optimisticData);
}
};
/**
* @param {array<object>} queryCalls
* @return {object} includes fields: requestedEdgeIDs, diffCalls
*/
GraphQLRange.prototype._retrieveRangeInfoForStaticCalls = function _retrieveRangeInfoForStaticCalls(queryCalls) {
var calls = _callsToString(queryCalls);
var storedInfo = this._staticQueriesMap[calls];
if (storedInfo) {
var _pageInfo;
return {
requestedEdgeIDs: storedInfo.edgeIDs,
diffCalls: [],
pageInfo: (_pageInfo = {}, _defineProperty(_pageInfo, START_CURSOR, storedInfo.cursors[0]), _defineProperty(_pageInfo, END_CURSOR, storedInfo.cursors[storedInfo.cursors.length - 1]), _defineProperty(_pageInfo, HAS_NEXT_PAGE, true), _defineProperty(_pageInfo, HAS_PREV_PAGE, true), _pageInfo)
};
}
// if we don't have the data for this static call already,
// return empty arrays with the corresponding diffCalls
return {
requestedEdgeIDs: [],
diffCalls: queryCalls,
pageInfo: RelayConnectionInterface.getDefaultPageInfo()
};
};
/**
* @param {array<object>} queryCalls
* @param {?object} optimisticData
* @return {object} includes fields: requestedEdgeIDs, diffCalls
*/
GraphQLRange.prototype._retrieveRangeInfoForFirstQuery = function _retrieveRangeInfoForFirstQuery(queryCalls, optimisticData) {
var appendEdgeIDs = [];
var prependEdgeIDs = [];
var deleteIDs = [];
if (optimisticData) {
appendEdgeIDs = optimisticData[GraphQLMutatorConstants.APPEND] || [];
prependEdgeIDs = optimisticData[GraphQLMutatorConstants.PREPEND] || [];
deleteIDs = optimisticData[GraphQLMutatorConstants.REMOVE] || [];
}
var calls = callsArrayToObject(queryCalls);
var countNeeded = calls.first + deleteIDs.length;
var segment;
var segmentIndex;
var pageInfo = _extends({}, RelayConnectionInterface.getDefaultPageInfo());
var afterCursor = calls.after;
if (afterCursor !== undefined) {
segmentIndex = this._getSegmentIndexByCursor(afterCursor);
if (segmentIndex == null) {
console.warn('GraphQLRange cannot find a segment that has the cursor: ' + afterCursor);
return {
requestedEdgeIDs: [],
diffCalls: [],
pageInfo: pageInfo
};
}
segment = this._orderedSegments[segmentIndex];
} else {
var prependEdgesCount = prependEdgeIDs.length;
countNeeded -= prependEdgesCount;
segmentIndex = 0;
segment = this._orderedSegments[segmentIndex];
}
var requestedMetadata = segment.getMetadataAfterCursor(countNeeded, afterCursor);
var requestedEdgeIDs = requestedMetadata.edgeIDs;
var requestedCursors = requestedMetadata.cursors;
var diffCalls = [];
if (requestedCursors.length) {
pageInfo[START_CURSOR] = requestedCursors[0];
pageInfo[END_CURSOR] = requestedCursors[requestedCursors.length - 1];
}
var lastID = requestedEdgeIDs[requestedEdgeIDs.length - 1];
// Only requested segment that does not include very last item from
// the range can have next page and diff calls
if (!this._hasLast || segmentIndex !== this._orderedSegments.length - 1 || lastID && lastID !== segment.getLastID()) {
pageInfo[HAS_NEXT_PAGE] = true;
if (requestedEdgeIDs.length < countNeeded) {
countNeeded -= requestedEdgeIDs.length;
var lastCursor = segment.getLastCursor();
// If segment has null cursors, retrieve whole range.
if (lastCursor === null) {
diffCalls.push({ name: 'first', value: calls.first });
} else {
if (lastCursor !== undefined) {
diffCalls.push({ name: 'after', value: lastCursor });
}
// If this is not the last segment, we should not request edges
// that would overlap the first element of the next segment.
if (segmentIndex !== this._orderedSegments.length - 1) {
var nextSegment = this._orderedSegments[segmentIndex + 1];
var firstCursor = nextSegment.getFirstCursor();
if (firstCursor !== undefined) {
diffCalls.push({ name: 'before', value: firstCursor });
}
}
diffCalls.push({ name: 'first', value: countNeeded });
}
}
}
if (optimisticData) {
if (prependEdgeIDs.length && !calls.after) {
requestedEdgeIDs = prependEdgeIDs.concat(requestedEdgeIDs);
}
if (appendEdgeIDs.length && !pageInfo[HAS_NEXT_PAGE]) {
requestedEdgeIDs = requestedEdgeIDs.concat(appendEdgeIDs);
}
if (deleteIDs.length) {
requestedEdgeIDs = requestedEdgeIDs.filter(function (edgeID) {
return deleteIDs.indexOf(edgeID) == -1;
});
}
if (requestedEdgeIDs.length > calls.first) {
requestedEdgeIDs = requestedEdgeIDs.slice(0, calls.first);
}
}
return {
requestedEdgeIDs: requestedEdgeIDs,
diffCalls: diffCalls,
pageInfo: pageInfo
};
};
/**
* @param {array<object>} queryCalls
* @param {?object} optimisticData
* @return {object} includes fields: requestedEdgeIDs, diffCalls
*/
GraphQLRange.prototype._retrieveRangeInfoForLastQuery = function _retrieveRangeInfoForLastQuery(queryCalls, optimisticData) {
var appendEdgeIDs = [];
var prependEdgeIDs = [];
var deleteIDs = [];
if (optimisticData) {
appendEdgeIDs = optimisticData[GraphQLMutatorConstants.APPEND] || [];
prependEdgeIDs = optimisticData[GraphQLMutatorConstants.PREPEND] || [];
deleteIDs = optimisticData[GraphQLMutatorConstants.REMOVE] || [];
}
var calls = callsArrayToObject(queryCalls);
var countNeeded = calls.last + deleteIDs.length;
var segment;
var segmentIndex;
var pageInfo = _extends({}, RelayConnectionInterface.getDefaultPageInfo());
var beforeCursor = calls.before;
if (beforeCursor !== undefined) {
segmentIndex = this._getSegmentIndexByCursor(beforeCursor);
if (segmentIndex == null) {
console.warn('GraphQLRange cannot find a segment that has the cursor: ' + beforeCursor);
return {
requestedEdgeIDs: [],
diffCalls: [],
pageInfo: pageInfo
};
}
segment = this._orderedSegments[segmentIndex];
} else {
var appendEdgesCount = appendEdgeIDs.length;
countNeeded -= appendEdgesCount;
segmentIndex = this._orderedSegments.length - 1;
segment = this._orderedSegments[segmentIndex];
}
var requestedMetadata = segment.getMetadataBeforeCursor(countNeeded, beforeCursor);
var requestedEdgeIDs = requestedMetadata.edgeIDs;
var requestedCursors = requestedMetadata.cursors;
var diffCalls = [];
if (requestedCursors.length) {
pageInfo[START_CURSOR] = requestedCursors[0];
pageInfo[END_CURSOR] = requestedCursors[requestedCursors.length - 1];
}
var firstID = requestedEdgeIDs[0];
// Only requested segment that does not include very first item from
// the range can have next page and diff calls
if (!this._hasFirst || segmentIndex !== 0 || firstID && firstID !== segment.getFirstID()) {
pageInfo[HAS_PREV_PAGE] = true;
if (requestedEdgeIDs.length < countNeeded) {
countNeeded -= requestedEdgeIDs.length;
var firstCursor = segment.getFirstCursor();
// If segment has null cursors, retrieve whole range.
if (firstCursor === null) {
diffCalls.push({ name: 'last', value: calls.last });
} else {
if (firstCursor !== undefined) {
diffCalls.push({ name: 'before', value: firstCursor });
}
// If this is not the first segment, we should not request edges
// that would overlap the last element of the previous segment.
if (segmentIndex !== 0) {
var prevSegment = this._orderedSegments[segmentIndex - 1];
var lastCursor = prevSegment.getLastCursor();
if (lastCursor !== undefined) {
diffCalls.push({ name: 'after', value: lastCursor });
}
}
diffCalls.push({ name: 'last', value: countNeeded });
}
}
}
if (optimisticData) {
if (appendEdgeIDs.length && !calls.before) {
requestedEdgeIDs = requestedEdgeIDs.concat(appendEdgeIDs);
}
if (prependEdgeIDs.length && !pageInfo[HAS_PREV_PAGE]) {
requestedEdgeIDs = prependEdgeIDs.concat(requestedEdgeIDs);
}
if (deleteIDs.length) {
requestedEdgeIDs = requestedEdgeIDs.filter(function (edgeID) {
return deleteIDs.indexOf(edgeID) == -1;
});
}
if (requestedEdgeIDs.length > calls.last) {
var length = requestedEdgeIDs.length;
requestedEdgeIDs = requestedEdgeIDs.slice(length - calls.last, length);
}
}
return {
requestedEdgeIDs: requestedEdgeIDs,
diffCalls: diffCalls,
pageInfo: pageInfo
};
};
GraphQLRange.fromJSON = function fromJSON(descriptor) {
var _descriptor = _slicedToArray(descriptor, 4);
var hasFirst = _descriptor[0];
var hasLast = _descriptor[1];
var staticQueriesMap = _descriptor[2];
var orderedSegments = _descriptor[3];
var range = new GraphQLRange();
range._hasFirst = hasFirst;
range._hasLast = hasLast;
range._staticQueriesMap = staticQueriesMap;
range._orderedSegments = orderedSegments.map(function (descriptor) {
return GraphQLSegment.fromJSON(descriptor);
});
return range;
};
GraphQLRange.prototype.toJSON = function toJSON() {
return [this._hasFirst, this._hasLast, this._staticQueriesMap, this._orderedSegments];
};
GraphQLRange.prototype.__debug = function __debug() {
return {
orderedSegments: this._orderedSegments
};
};
GraphQLRange.prototype.getEdgeIDs = function getEdgeIDs() {
var edgeIDs = [];
this._orderedSegments.forEach(function (segment) {
edgeIDs.push.apply(edgeIDs, _toConsumableArray(segment.getEdgeIDs()));
});
forEachObject(this._staticQueriesMap, function (query) {
edgeIDs.push.apply(edgeIDs, _toConsumableArray(query.edgeIDs));
});
return edgeIDs;
};
return GraphQLRange;
})();
function _callsToString(calls) {
return calls.map(function (call) {
return serializeRelayQueryCall(call).substring(1);
}).join(',');
}
module.exports = GraphQLRange;