src/legacy/store/GraphQLRange.js
/**
* 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 GraphQLRange
* @typechecks
*/
'use strict';
const GraphQLMutatorConstants = require('GraphQLMutatorConstants');
const GraphQLSegment = require('GraphQLSegment');
const RelayConnectionInterface = require('RelayConnectionInterface');
const RelayRecord = require('RelayRecord');
const forEachObject = require('forEachObject');
const invariant = require('invariant');
const rangeOperationToMetadataKey = require('rangeOperationToMetadataKey');
const serializeRelayQueryCall = require('serializeRelayQueryCall');
const warning = require('warning');
const {
END_CURSOR,
HAS_NEXT_PAGE,
HAS_PREV_PAGE,
START_CURSOR,
} = RelayConnectionInterface;
/**
* @param {array<object>} queryCalls
* @return {object}
*/
function callsArrayToObject(queryCalls) {
const calls = {};
for (let ii = 0; ii < queryCalls.length; ii++) {
if (RelayConnectionInterface.isConnectionCall(queryCalls[ii])) {
const queryCall = queryCalls[ii];
let {value} = queryCall;
// 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[queryCall.name] = value;
}
}
// update first and last call values to be numbers
if (calls.first) {
invariant(
!isNaN(calls.first),
'GraphQLRange: Expected `first` argument to be a number, got ' +
'`%s`.',
calls.first
);
calls.first = +calls.first;
} else if (calls.last) {
invariant(
!isNaN(calls.last),
'GraphQLRange: Expected `last` argument to be a number, got ' +
'`%s`.',
calls.last
);
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) {
const hasFirst = calls.hasOwnProperty('first');
const 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) {
invariant(
RelayRecord.getDataIDForObject(edge) !== undefined,
'GraphQLStore: `edge` must have a data id'
);
invariant(
edge.node !== undefined,
'GraphQLStore: `edge` must have `node` field'
);
}
/**
* @param {array<object>} edges
*/
function validateEdges(edges) {
edges.forEach(validateEdge);
}
/**
* @internal
*
* 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 http://facebook.github.io/relay/docs/graphql-connections.html
*/
class GraphQLRange {
constructor() {
this.reset();
}
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
*/
_resetSegment(index) {
invariant(
index >= 0 && index < this._orderedSegments.length,
'cannot reset non-existent segment'
);
this._orderedSegments[index] = new GraphQLSegment();
}
/**
* @param {string} cursor
* @return {?number}
*/
_getSegmentIndexByCursor(cursor) {
let deletedIndex = null;
// TODO: revisit if we end up having too many segments
for (let ii = 0; ii < this._orderedSegments.length; ii++) {
if (this._orderedSegments[ii].containsEdgeWithCursor(cursor)) {
return ii;
} else if (this._orderedSegments[ii].containsEdgeWithCursor(
cursor,
true
)) {
deletedIndex = ii;
}
}
return deletedIndex;
}
/**
* @param {string} id
* @return {?number}
*/
_getSegmentIndexByID(id) {
// TODO: revisit if we end up having too many segments
for (let 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
*/
_addStaticEdges(queryCalls, edges) {
const calls = _callsToString(queryCalls);
const edgeIDsToStore = [];
const cursorsToStore = [];
for (let ii = 0; ii < edges.length; ii++) {
const edge = edges[ii];
edgeIDsToStore.push(RelayRecord.getDataIDForObject(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
*/
addItems(queryCalls, edges, pageInfo) {
validateEdges(edges);
const calls = callsArrayToObject(queryCalls);
let 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 and there are new edges
if (pageInfo[HAS_NEXT_PAGE] === true && edges.length !== 0) {
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 and there are new edges
if (pageInfo[HAS_PREV_PAGE] === true && edges.length !== 0) {
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}
*/
getFirstSegment() {
return this._orderedSegments[0];
}
/**
* @return {GraphQLSegment}
*/
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
*/
_concatSegments(segmentIndex) {
invariant(
(segmentIndex + 1) < this._orderedSegments.length &&
segmentIndex >= 0,
'GraphQLRange cannot concat segments outside the range ' +
'of orderedSegments'
);
const firstSegment = this._orderedSegments[segmentIndex];
const 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
*/
prependEdge(edge) {
validateEdge(edge);
this._hasFirst = true;
this._removeEdgeIfApplicable(edge);
const 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
*/
appendEdge(edge) {
validateEdge(edge);
this._hasLast = true;
this._removeEdgeIfApplicable(edge);
const segment = this.getLastSegment();
segment.appendEdge(edge);
}
/**
* Removes edge in range if it matches id in input edge.
* @param {object} edge
*/
_removeEdgeIfApplicable(edge) {
const id = RelayRecord.getDataIDForObject(edge);
const 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
*/
_removeEdgesIfApplicable(edges) {
for (let 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
*/
_addAfterFirstItems(
edges,
hasNextPage,
afterCursor,
beforeCursor
) {
let segment;
let segmentIndex;
let lastCursor;
if (afterCursor !== undefined) {
segmentIndex = this._getSegmentIndexByCursor(afterCursor);
if (segmentIndex == null) {
warning(
false,
'GraphQLRange cannot find a segment that has the cursor: %s',
afterCursor
);
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;
}
}
}
const nextSegment = this._orderedSegments[segmentIndex + 1];
if (beforeCursor !== undefined) {
if (segmentIndex === this._orderedSegments.length - 1) {
console.warn(
'GraphQLRange cannot add because there is no next segment'
);
return;
} else if (!nextSegment.isFirstCursor(beforeCursor)) {
warning(
false,
'GraphQLRange cannot add because beforeCursor does not match first ' +
'cursor of the next segment'
);
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
*/
_reconcileAfterFirstEdges(segment, edges, cursor) {
const metadata = segment.getMetadataAfterCursor(edges.length + 1, cursor);
const edgeIDs = metadata.edgeIDs;
if (edgeIDs.length > edges.length) {
// Already have more edges than the input.
return undefined;
}
for (let ii = 0; ii < edgeIDs.length; ii++) {
if (edgeIDs[ii] !== RelayRecord.getDataIDForObject(edges[ii])) {
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.'
);
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
*/
_addBeforeLastItems(
edges,
hasPrevPage,
beforeCursor,
afterCursor
) {
let segment;
let segmentIndex;
let firstCursor;
if (beforeCursor !== undefined) {
segmentIndex = this._getSegmentIndexByCursor(beforeCursor);
if (segmentIndex == null) {
warning(
false,
'GraphQLRange cannot find a segment that has the cursor: %s',
beforeCursor
);
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;
}
}
}
const prevSegment = this._orderedSegments[segmentIndex - 1];
if (afterCursor !== undefined) {
if (segmentIndex === 0) {
console.warn(
'GraphQLRange cannot add because there is no previous segment'
);
return;
} else if (!prevSegment.isLastCursor(afterCursor)) {
warning(
false,
'GraphQLRange cannot add because afterCursor does not match last ' +
'cursor of the previous segment'
);
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
*/
_reconcileBeforeLastEdges(segment, edges, cursor) {
const metadata = segment.getMetadataBeforeCursor(edges.length + 1, cursor);
const edgeIDs = metadata.edgeIDs;
if (edgeIDs.length > edges.length) {
// Already have more edges than the input.
return undefined;
}
for (let ii = 1; ii <= edgeIDs.length; ii++) {
if (edgeIDs[edgeIDs.length - ii] !==
RelayRecord.getDataIDForObject(edges[edges.length - ii])) {
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.'
);
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
*/
removeEdgeWithID(id) {
for (let ii = 0; ii < this._orderedSegments.length; ii++) {
this._orderedSegments[ii].removeAllEdges(id);
}
}
/**
* @param {array<object>} queryCalls
* @param {?object} queuedRecord
* @return {object} includes fields: requestedEdgeIDs, diffCalls
*/
retrieveRangeInfoForQuery(queryCalls, queuedRecord) {
const 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,
queuedRecord
);
} else if (calls.last) {
return this._retrieveRangeInfoForLastQuery(
queryCalls,
queuedRecord
);
}
}
/**
* @param {array<object>} queryCalls
* @return {object} includes fields: requestedEdgeIDs, diffCalls
*/
_retrieveRangeInfoForStaticCalls(queryCalls) {
const calls = _callsToString(queryCalls);
const storedInfo = this._staticQueriesMap[calls];
if (storedInfo) {
return {
requestedEdgeIDs: storedInfo.edgeIDs,
diffCalls: [],
pageInfo: {
[START_CURSOR]: storedInfo.cursors[0],
[END_CURSOR]: storedInfo.cursors[storedInfo.cursors.length - 1],
[HAS_NEXT_PAGE]: true,
[HAS_PREV_PAGE]: true,
},
};
}
// 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 {object} queuedRecord
* @return {?array<string>}
*/
_getAppendedIDsForQueuedRecord(queuedRecord) {
return queuedRecord[
rangeOperationToMetadataKey[GraphQLMutatorConstants.APPEND]
];
}
/**
* @param {object} queuedRecord
* @return {?array<string>}
*/
_getRemovedIDsForQueuedRecord(queuedRecord) {
return queuedRecord[
rangeOperationToMetadataKey[GraphQLMutatorConstants.REMOVE]
];
}
/**
* @param {object} queuedRecord
* @return {?array<string>}
*/
_getPrependedIDsForQueuedRecord(queuedRecord) {
return queuedRecord[
rangeOperationToMetadataKey[GraphQLMutatorConstants.PREPEND]
];
}
/**
* @param {array<object>} queryCalls
* @param {?object} queuedRecord
* @return {object} includes fields: requestedEdgeIDs, diffCalls
*/
_retrieveRangeInfoForFirstQuery(
queryCalls,
queuedRecord
) {
let appendEdgeIDs;
let prependEdgeIDs;
let removeIDs;
if (queuedRecord) {
appendEdgeIDs = this._getAppendedIDsForQueuedRecord(queuedRecord);
prependEdgeIDs = this._getPrependedIDsForQueuedRecord(queuedRecord);
removeIDs = this._getRemovedIDsForQueuedRecord(queuedRecord);
}
const calls = callsArrayToObject(queryCalls);
let countNeeded = calls.first + (removeIDs ? removeIDs.length : 0);
let segment;
let segmentIndex;
const pageInfo = {
...RelayConnectionInterface.getDefaultPageInfo(),
};
const 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 {
const prependEdgesCount = prependEdgeIDs ? prependEdgeIDs.length : 0;
countNeeded -= prependEdgesCount;
segmentIndex = 0;
segment = this._orderedSegments[segmentIndex];
}
const requestedMetadata =
segment.getMetadataAfterCursor(countNeeded, afterCursor);
let requestedEdgeIDs = requestedMetadata.edgeIDs;
const requestedCursors = requestedMetadata.cursors;
const diffCalls = [];
if (requestedCursors.length) {
pageInfo[START_CURSOR] = requestedCursors[0];
pageInfo[END_CURSOR] = requestedCursors[requestedCursors.length - 1];
}
const 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;
const 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) {
const nextSegment = this._orderedSegments[segmentIndex + 1];
const firstCursor = nextSegment.getFirstCursor();
if (firstCursor !== undefined) {
diffCalls.push({name: 'before', value: firstCursor});
}
}
diffCalls.push({name: 'first', value: countNeeded});
}
}
}
if (queuedRecord) {
if (prependEdgeIDs && prependEdgeIDs.length && !calls.after) {
requestedEdgeIDs = prependEdgeIDs.concat(requestedEdgeIDs);
}
if (appendEdgeIDs && appendEdgeIDs.length && !pageInfo[HAS_NEXT_PAGE]) {
requestedEdgeIDs = requestedEdgeIDs.concat(appendEdgeIDs);
}
if (removeIDs && removeIDs.length) {
requestedEdgeIDs = requestedEdgeIDs.filter(function(edgeID) {
return (removeIDs.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} queuedRecord
* @return {object} includes fields: requestedEdgeIDs, diffCalls
*/
_retrieveRangeInfoForLastQuery(
queryCalls,
queuedRecord
) {
let appendEdgeIDs;
let prependEdgeIDs;
let removeIDs;
if (queuedRecord) {
appendEdgeIDs = this._getAppendedIDsForQueuedRecord(queuedRecord);
prependEdgeIDs = this._getPrependedIDsForQueuedRecord(queuedRecord);
removeIDs = this._getRemovedIDsForQueuedRecord(queuedRecord);
}
const calls = callsArrayToObject(queryCalls);
let countNeeded = calls.last + (removeIDs ? removeIDs.length : 0);
let segment;
let segmentIndex;
const pageInfo = {
...RelayConnectionInterface.getDefaultPageInfo(),
};
const 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 {
const appendEdgesCount = appendEdgeIDs ? appendEdgeIDs.length : 0;
countNeeded -= appendEdgesCount;
segmentIndex = this._orderedSegments.length - 1;
segment = this._orderedSegments[segmentIndex];
}
const requestedMetadata =
segment.getMetadataBeforeCursor(countNeeded, beforeCursor);
let requestedEdgeIDs = requestedMetadata.edgeIDs;
const requestedCursors = requestedMetadata.cursors;
const diffCalls = [];
if (requestedCursors.length) {
pageInfo[START_CURSOR] = requestedCursors[0];
pageInfo[END_CURSOR] = requestedCursors[requestedCursors.length - 1];
}
const 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;
const 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) {
const prevSegment = this._orderedSegments[segmentIndex - 1];
const lastCursor = prevSegment.getLastCursor();
if (lastCursor !== undefined) {
diffCalls.push({name: 'after', value: lastCursor});
}
}
diffCalls.push({name: 'last', value: countNeeded});
}
}
}
if (queuedRecord) {
if (appendEdgeIDs && appendEdgeIDs.length && !calls.before) {
requestedEdgeIDs = requestedEdgeIDs.concat(appendEdgeIDs);
}
if (prependEdgeIDs && prependEdgeIDs.length && !pageInfo[HAS_PREV_PAGE]) {
requestedEdgeIDs = prependEdgeIDs.concat(requestedEdgeIDs);
}
if (removeIDs && removeIDs.length) {
requestedEdgeIDs = requestedEdgeIDs.filter(function(edgeID) {
return (removeIDs.indexOf(edgeID) === -1);
});
}
if (requestedEdgeIDs.length > calls.last) {
const length = requestedEdgeIDs.length;
requestedEdgeIDs = requestedEdgeIDs.slice(length - calls.last, length);
}
}
return {
requestedEdgeIDs: requestedEdgeIDs,
diffCalls: diffCalls,
pageInfo: pageInfo,
};
}
static fromJSON(descriptor) {
const [
hasFirst,
hasLast,
staticQueriesMap,
orderedSegments,
] = descriptor;
const range = new GraphQLRange();
range._hasFirst = hasFirst;
range._hasLast = hasLast;
range._staticQueriesMap = staticQueriesMap;
range._orderedSegments = orderedSegments.map(
segmentDescriptor => GraphQLSegment.fromJSON(segmentDescriptor)
);
return range;
}
toJSON() {
return [
this._hasFirst,
this._hasLast,
this._staticQueriesMap,
this._orderedSegments,
];
}
__debug() {
return {
orderedSegments: this._orderedSegments,
};
}
getEdgeIDs() {
const edgeIDs = [];
this._orderedSegments.forEach(segment => {
edgeIDs.push(...segment.getEdgeIDs());
});
forEachObject(this._staticQueriesMap, (query) => {
edgeIDs.push(...query.edgeIDs);
});
return edgeIDs;
}
getSegmentedEdgeIDs() {
return this._orderedSegments.map(segment => segment.getEdgeIDs());
}
}
/**
* @param {array<object>} calls
* @return {string}
*/
function _callsToString(calls) {
return calls.map(
call => serializeRelayQueryCall(call).substring(1)
).join(',');
}
module.exports = GraphQLRange;