512 lines
15 KiB
JavaScript
512 lines
15 KiB
JavaScript
/**
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
*
|
|
* @format
|
|
*/
|
|
'use strict';
|
|
|
|
var CONNECTION = 'connection'; // Per-instance incrementing index used to generate unique edge IDs
|
|
|
|
var NEXT_EDGE_INDEX = '__connection_next_edge_index';
|
|
/**
|
|
* @public
|
|
*
|
|
* A default runtime handler for connection fields that appends newly fetched
|
|
* edges onto the end of a connection, regardless of the arguments used to fetch
|
|
* those edges.
|
|
*/
|
|
|
|
function update(store, payload) {
|
|
var record = store.get(payload.dataID);
|
|
|
|
if (!record) {
|
|
return;
|
|
}
|
|
|
|
var _RelayConnectionInter = require("./RelayConnectionInterface").get(),
|
|
EDGES = _RelayConnectionInter.EDGES,
|
|
END_CURSOR = _RelayConnectionInter.END_CURSOR,
|
|
HAS_NEXT_PAGE = _RelayConnectionInter.HAS_NEXT_PAGE,
|
|
HAS_PREV_PAGE = _RelayConnectionInter.HAS_PREV_PAGE,
|
|
PAGE_INFO = _RelayConnectionInter.PAGE_INFO,
|
|
PAGE_INFO_TYPE = _RelayConnectionInter.PAGE_INFO_TYPE,
|
|
START_CURSOR = _RelayConnectionInter.START_CURSOR;
|
|
|
|
var serverConnection = record.getLinkedRecord(payload.fieldKey);
|
|
var serverPageInfo = serverConnection && serverConnection.getLinkedRecord(PAGE_INFO);
|
|
|
|
if (!serverConnection) {
|
|
record.setValue(null, payload.handleKey);
|
|
return;
|
|
}
|
|
|
|
var clientConnection = record.getLinkedRecord(payload.handleKey);
|
|
var clientPageInfo = clientConnection && clientConnection.getLinkedRecord(PAGE_INFO);
|
|
|
|
if (!clientConnection) {
|
|
// Initial fetch with data: copy fields from the server record
|
|
var connection = store.create(require("./generateRelayClientID")(record.getDataID(), payload.handleKey), serverConnection.getType());
|
|
connection.setValue(0, NEXT_EDGE_INDEX);
|
|
connection.copyFieldsFrom(serverConnection);
|
|
var serverEdges = serverConnection.getLinkedRecords(EDGES);
|
|
|
|
if (serverEdges) {
|
|
serverEdges = serverEdges.map(function (edge) {
|
|
return buildConnectionEdge(store, connection, edge);
|
|
});
|
|
connection.setLinkedRecords(serverEdges, EDGES);
|
|
}
|
|
|
|
record.setLinkedRecord(connection, payload.handleKey);
|
|
clientPageInfo = store.create(require("./generateRelayClientID")(connection.getDataID(), PAGE_INFO), PAGE_INFO_TYPE);
|
|
clientPageInfo.setValue(false, HAS_NEXT_PAGE);
|
|
clientPageInfo.setValue(false, HAS_PREV_PAGE);
|
|
clientPageInfo.setValue(null, END_CURSOR);
|
|
clientPageInfo.setValue(null, START_CURSOR);
|
|
|
|
if (serverPageInfo) {
|
|
clientPageInfo.copyFieldsFrom(serverPageInfo);
|
|
}
|
|
|
|
connection.setLinkedRecord(clientPageInfo, PAGE_INFO);
|
|
} else {
|
|
var _connection = clientConnection; // Subsequent fetches:
|
|
// - updated fields on the connection
|
|
// - merge prev/next edges, de-duplicating by node id
|
|
// - synthesize page info fields
|
|
|
|
var _serverEdges = serverConnection.getLinkedRecords(EDGES);
|
|
|
|
if (_serverEdges) {
|
|
_serverEdges = _serverEdges.map(function (edge) {
|
|
return buildConnectionEdge(store, _connection, edge);
|
|
});
|
|
}
|
|
|
|
var prevEdges = _connection.getLinkedRecords(EDGES);
|
|
|
|
var prevPageInfo = _connection.getLinkedRecord(PAGE_INFO);
|
|
|
|
_connection.copyFieldsFrom(serverConnection); // Reset EDGES and PAGE_INFO fields
|
|
|
|
|
|
if (prevEdges) {
|
|
_connection.setLinkedRecords(prevEdges, EDGES);
|
|
}
|
|
|
|
if (prevPageInfo) {
|
|
_connection.setLinkedRecord(prevPageInfo, PAGE_INFO);
|
|
}
|
|
|
|
var nextEdges = [];
|
|
var args = payload.args;
|
|
|
|
if (prevEdges && _serverEdges) {
|
|
if (args.after != null) {
|
|
// Forward pagination from the end of the connection: append edges
|
|
if (clientPageInfo && args.after === clientPageInfo.getValue(END_CURSOR)) {
|
|
var nodeIDs = new Set();
|
|
mergeEdges(prevEdges, nextEdges, nodeIDs);
|
|
mergeEdges(_serverEdges, nextEdges, nodeIDs);
|
|
} else {
|
|
process.env.NODE_ENV !== "production" ? require("fbjs/lib/warning")(false, 'RelayConnectionHandler: Unexpected after cursor `%s`, edges must ' + 'be fetched from the end of the list (`%s`).', args.after, clientPageInfo && clientPageInfo.getValue(END_CURSOR)) : void 0;
|
|
return;
|
|
}
|
|
} else if (args.before != null) {
|
|
// Backward pagination from the start of the connection: prepend edges
|
|
if (clientPageInfo && args.before === clientPageInfo.getValue(START_CURSOR)) {
|
|
var _nodeIDs = new Set();
|
|
|
|
mergeEdges(_serverEdges, nextEdges, _nodeIDs);
|
|
mergeEdges(prevEdges, nextEdges, _nodeIDs);
|
|
} else {
|
|
process.env.NODE_ENV !== "production" ? require("fbjs/lib/warning")(false, 'RelayConnectionHandler: Unexpected before cursor `%s`, edges must ' + 'be fetched from the beginning of the list (`%s`).', args.before, clientPageInfo && clientPageInfo.getValue(START_CURSOR)) : void 0;
|
|
return;
|
|
}
|
|
} else {
|
|
// The connection was refetched from the beginning/end: replace edges
|
|
nextEdges = _serverEdges;
|
|
}
|
|
} else if (_serverEdges) {
|
|
nextEdges = _serverEdges;
|
|
} else {
|
|
nextEdges = prevEdges;
|
|
} // Update edges only if they were updated, the null check is
|
|
// for Flow (prevEdges could be null).
|
|
|
|
|
|
if (nextEdges != null && nextEdges !== prevEdges) {
|
|
_connection.setLinkedRecords(nextEdges, EDGES);
|
|
} // Page info should be updated even if no new edge were returned.
|
|
|
|
|
|
if (clientPageInfo && serverPageInfo) {
|
|
if (args.after == null && args.before == null) {
|
|
// The connection was refetched from the beginning/end: replace
|
|
// page_info
|
|
clientPageInfo.copyFieldsFrom(serverPageInfo);
|
|
} else if (args.before != null || args.after == null && args.last) {
|
|
clientPageInfo.setValue(!!serverPageInfo.getValue(HAS_PREV_PAGE), HAS_PREV_PAGE);
|
|
var startCursor = serverPageInfo.getValue(START_CURSOR);
|
|
|
|
if (typeof startCursor === 'string') {
|
|
clientPageInfo.setValue(startCursor, START_CURSOR);
|
|
}
|
|
} else if (args.after != null || args.before == null && args.first) {
|
|
clientPageInfo.setValue(!!serverPageInfo.getValue(HAS_NEXT_PAGE), HAS_NEXT_PAGE);
|
|
var endCursor = serverPageInfo.getValue(END_CURSOR);
|
|
|
|
if (typeof endCursor === 'string') {
|
|
clientPageInfo.setValue(endCursor, END_CURSOR);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* @public
|
|
*
|
|
* Given a record and the name of the schema field for which a connection was
|
|
* fetched, returns the linked connection record.
|
|
*
|
|
* Example:
|
|
*
|
|
* Given that data has already been fetched on some user `<id>` on the `friends`
|
|
* field:
|
|
*
|
|
* ```
|
|
* fragment FriendsFragment on User {
|
|
* friends(first: 10) @connection(key: "FriendsFragment_friends") {
|
|
* edges {
|
|
* node {
|
|
* id
|
|
* }
|
|
* }
|
|
* }
|
|
* }
|
|
* ```
|
|
*
|
|
* The `friends` connection record can be accessed with:
|
|
*
|
|
* ```
|
|
* store => {
|
|
* const user = store.get('<id>');
|
|
* const friends = RelayConnectionHandler.getConnection(user, 'FriendsFragment_friends');
|
|
* // Access fields on the connection:
|
|
* const edges = friends.getLinkedRecords('edges');
|
|
* }
|
|
* ```
|
|
*
|
|
* TODO: t15733312
|
|
* Currently we haven't run into this case yet, but we need to add a `getConnections`
|
|
* that returns an array of the connections under the same `key` regardless of the variables.
|
|
*/
|
|
|
|
|
|
function getConnection(record, key, filters) {
|
|
var handleKey = require("./getRelayHandleKey")(CONNECTION, key, null);
|
|
|
|
return record.getLinkedRecord(handleKey, filters);
|
|
}
|
|
/**
|
|
* @public
|
|
*
|
|
* Inserts an edge after the given cursor, or at the end of the list if no
|
|
* cursor is provided.
|
|
*
|
|
* Example:
|
|
*
|
|
* Given that data has already been fetched on some user `<id>` on the `friends`
|
|
* field:
|
|
*
|
|
* ```
|
|
* fragment FriendsFragment on User {
|
|
* friends(first: 10) @connection(key: "FriendsFragment_friends") {
|
|
* edges {
|
|
* node {
|
|
* id
|
|
* }
|
|
* }
|
|
* }
|
|
* }
|
|
* ```
|
|
*
|
|
* An edge can be appended with:
|
|
*
|
|
* ```
|
|
* store => {
|
|
* const user = store.get('<id>');
|
|
* const friends = RelayConnectionHandler.getConnection(user, 'FriendsFragment_friends');
|
|
* const edge = store.create('<edge-id>', 'FriendsEdge');
|
|
* RelayConnectionHandler.insertEdgeAfter(friends, edge);
|
|
* }
|
|
* ```
|
|
*/
|
|
|
|
|
|
function insertEdgeAfter(record, newEdge, cursor) {
|
|
var _RelayConnectionInter2 = require("./RelayConnectionInterface").get(),
|
|
CURSOR = _RelayConnectionInter2.CURSOR,
|
|
EDGES = _RelayConnectionInter2.EDGES;
|
|
|
|
var edges = record.getLinkedRecords(EDGES);
|
|
|
|
if (!edges) {
|
|
record.setLinkedRecords([newEdge], EDGES);
|
|
return;
|
|
}
|
|
|
|
var nextEdges;
|
|
|
|
if (cursor == null) {
|
|
nextEdges = edges.concat(newEdge);
|
|
} else {
|
|
nextEdges = [];
|
|
var foundCursor = false;
|
|
|
|
for (var ii = 0; ii < edges.length; ii++) {
|
|
var edge = edges[ii];
|
|
nextEdges.push(edge);
|
|
|
|
if (edge == null) {
|
|
continue;
|
|
}
|
|
|
|
var edgeCursor = edge.getValue(CURSOR);
|
|
|
|
if (cursor === edgeCursor) {
|
|
nextEdges.push(newEdge);
|
|
foundCursor = true;
|
|
}
|
|
}
|
|
|
|
if (!foundCursor) {
|
|
nextEdges.push(newEdge);
|
|
}
|
|
}
|
|
|
|
record.setLinkedRecords(nextEdges, EDGES);
|
|
}
|
|
/**
|
|
* @public
|
|
*
|
|
* Creates an edge for a connection record, given a node and edge type.
|
|
*/
|
|
|
|
|
|
function createEdge(store, record, node, edgeType) {
|
|
var _RelayConnectionInter3 = require("./RelayConnectionInterface").get(),
|
|
NODE = _RelayConnectionInter3.NODE; // An index-based client ID could easily conflict (unless it was
|
|
// auto-incrementing, but there is nowhere to the store the id)
|
|
// Instead, construct a client ID based on the connection ID and node ID,
|
|
// which will only conflict if the same node is added to the same connection
|
|
// twice. This is acceptable since the `insertEdge*` functions ignore
|
|
// duplicates.
|
|
|
|
|
|
var edgeID = require("./generateRelayClientID")(record.getDataID(), node.getDataID());
|
|
|
|
var edge = store.get(edgeID);
|
|
|
|
if (!edge) {
|
|
edge = store.create(edgeID, edgeType);
|
|
}
|
|
|
|
edge.setLinkedRecord(node, NODE);
|
|
return edge;
|
|
}
|
|
/**
|
|
* @public
|
|
*
|
|
* Inserts an edge before the given cursor, or at the beginning of the list if
|
|
* no cursor is provided.
|
|
*
|
|
* Example:
|
|
*
|
|
* Given that data has already been fetched on some user `<id>` on the `friends`
|
|
* field:
|
|
*
|
|
* ```
|
|
* fragment FriendsFragment on User {
|
|
* friends(first: 10) @connection(key: "FriendsFragment_friends") {
|
|
* edges {
|
|
* node {
|
|
* id
|
|
* }
|
|
* }
|
|
* }
|
|
* }
|
|
* ```
|
|
*
|
|
* An edge can be prepended with:
|
|
*
|
|
* ```
|
|
* store => {
|
|
* const user = store.get('<id>');
|
|
* const friends = RelayConnectionHandler.getConnection(user, 'FriendsFragment_friends');
|
|
* const edge = store.create('<edge-id>', 'FriendsEdge');
|
|
* RelayConnectionHandler.insertEdgeBefore(friends, edge);
|
|
* }
|
|
* ```
|
|
*/
|
|
|
|
|
|
function insertEdgeBefore(record, newEdge, cursor) {
|
|
var _RelayConnectionInter4 = require("./RelayConnectionInterface").get(),
|
|
CURSOR = _RelayConnectionInter4.CURSOR,
|
|
EDGES = _RelayConnectionInter4.EDGES;
|
|
|
|
var edges = record.getLinkedRecords(EDGES);
|
|
|
|
if (!edges) {
|
|
record.setLinkedRecords([newEdge], EDGES);
|
|
return;
|
|
}
|
|
|
|
var nextEdges;
|
|
|
|
if (cursor == null) {
|
|
nextEdges = [newEdge].concat(edges);
|
|
} else {
|
|
nextEdges = [];
|
|
var foundCursor = false;
|
|
|
|
for (var ii = 0; ii < edges.length; ii++) {
|
|
var edge = edges[ii];
|
|
|
|
if (edge != null) {
|
|
var edgeCursor = edge.getValue(CURSOR);
|
|
|
|
if (cursor === edgeCursor) {
|
|
nextEdges.push(newEdge);
|
|
foundCursor = true;
|
|
}
|
|
}
|
|
|
|
nextEdges.push(edge);
|
|
}
|
|
|
|
if (!foundCursor) {
|
|
nextEdges.unshift(newEdge);
|
|
}
|
|
}
|
|
|
|
record.setLinkedRecords(nextEdges, EDGES);
|
|
}
|
|
/**
|
|
* @public
|
|
*
|
|
* Remove any edges whose `node.id` matches the given id.
|
|
*/
|
|
|
|
|
|
function deleteNode(record, nodeID) {
|
|
var _RelayConnectionInter5 = require("./RelayConnectionInterface").get(),
|
|
EDGES = _RelayConnectionInter5.EDGES,
|
|
NODE = _RelayConnectionInter5.NODE;
|
|
|
|
var edges = record.getLinkedRecords(EDGES);
|
|
|
|
if (!edges) {
|
|
return;
|
|
}
|
|
|
|
var nextEdges;
|
|
|
|
for (var ii = 0; ii < edges.length; ii++) {
|
|
var edge = edges[ii];
|
|
var node = edge && edge.getLinkedRecord(NODE);
|
|
|
|
if (node != null && node.getDataID() === nodeID) {
|
|
if (nextEdges === undefined) {
|
|
nextEdges = edges.slice(0, ii);
|
|
}
|
|
} else if (nextEdges !== undefined) {
|
|
nextEdges.push(edge);
|
|
}
|
|
}
|
|
|
|
if (nextEdges !== undefined) {
|
|
record.setLinkedRecords(nextEdges, EDGES);
|
|
}
|
|
}
|
|
/**
|
|
* @internal
|
|
*
|
|
* Creates a copy of an edge with a unique ID based on per-connection-instance
|
|
* incrementing edge index. This is necessary to avoid collisions between edges,
|
|
* which can occur because (edge) client IDs are assigned deterministically
|
|
* based on the path from the nearest node with an id.
|
|
*
|
|
* Example: if the first N edges of the same connection are refetched, the edges
|
|
* from the second fetch will be assigned the same IDs as the first fetch, even
|
|
* though the nodes they point to may be different (or the same and in different
|
|
* order).
|
|
*/
|
|
|
|
|
|
function buildConnectionEdge(store, connection, edge) {
|
|
if (edge == null) {
|
|
return edge;
|
|
}
|
|
|
|
var _RelayConnectionInter6 = require("./RelayConnectionInterface").get(),
|
|
EDGES = _RelayConnectionInter6.EDGES;
|
|
|
|
var edgeIndex = connection.getValue(NEXT_EDGE_INDEX);
|
|
!(typeof edgeIndex === 'number') ? process.env.NODE_ENV !== "production" ? require("fbjs/lib/invariant")(false, 'RelayConnectionHandler: Expected %s to be a number, got `%s`.', NEXT_EDGE_INDEX, edgeIndex) : require("fbjs/lib/invariant")(false) : void 0;
|
|
|
|
var edgeID = require("./generateRelayClientID")(connection.getDataID(), EDGES, edgeIndex);
|
|
|
|
var connectionEdge = store.create(edgeID, edge.getType());
|
|
connectionEdge.copyFieldsFrom(edge);
|
|
connection.setValue(edgeIndex + 1, NEXT_EDGE_INDEX);
|
|
return connectionEdge;
|
|
}
|
|
/**
|
|
* @internal
|
|
*
|
|
* Adds the source edges to the target edges, skipping edges with
|
|
* duplicate node ids.
|
|
*/
|
|
|
|
|
|
function mergeEdges(sourceEdges, targetEdges, nodeIDs) {
|
|
var _RelayConnectionInter7 = require("./RelayConnectionInterface").get(),
|
|
NODE = _RelayConnectionInter7.NODE;
|
|
|
|
for (var ii = 0; ii < sourceEdges.length; ii++) {
|
|
var edge = sourceEdges[ii];
|
|
|
|
if (!edge) {
|
|
continue;
|
|
}
|
|
|
|
var node = edge.getLinkedRecord(NODE);
|
|
var nodeID = node && node.getValue('id');
|
|
|
|
if (nodeID) {
|
|
if (nodeIDs.has(nodeID)) {
|
|
continue;
|
|
}
|
|
|
|
nodeIDs.add(nodeID);
|
|
}
|
|
|
|
targetEdges.push(edge);
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
buildConnectionEdge: buildConnectionEdge,
|
|
createEdge: createEdge,
|
|
deleteNode: deleteNode,
|
|
getConnection: getConnection,
|
|
insertEdgeAfter: insertEdgeAfter,
|
|
insertEdgeBefore: insertEdgeBefore,
|
|
update: update
|
|
}; |