react-relay
Version:
A framework for building data-driven React applications.
414 lines (370 loc) • 14.9 kB
JavaScript
/**
* 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 writeRelayUpdatePayload
*
* @typechecks
*/
'use strict';
var _defineProperty = require('babel-runtime/helpers/define-property')['default'];
var _extends = require('babel-runtime/helpers/extends')['default'];
var GraphQLMutatorConstants = require('./GraphQLMutatorConstants');
var RelayConnectionInterface = require('./RelayConnectionInterface');
var RelayMutationTracker = require('./RelayMutationTracker');
var RelayMutationType = require('./RelayMutationType');
var RelayNodeInterface = require('./RelayNodeInterface');
var RelayQuery = require('./RelayQuery');
var RelayQueryPath = require('./RelayQueryPath');
var RelayProfiler = require('./RelayProfiler');
var RelayRecordStatus = require('./RelayRecordStatus');
var generateClientEdgeID = require('./generateClientEdgeID');
var generateClientID = require('./generateClientID');
var invariant = require('fbjs/lib/invariant');
var printRelayQueryCall = require('./printRelayQueryCall');
var warning = require('fbjs/lib/warning');
var CLIENT_MUTATION_ID = RelayConnectionInterface.CLIENT_MUTATION_ID;
var EDGES = RelayConnectionInterface.EDGES;
var APPEND = GraphQLMutatorConstants.APPEND;
var PREPEND = GraphQLMutatorConstants.PREPEND;
var REMOVE = GraphQLMutatorConstants.REMOVE;
var EDGES_FIELD = RelayQuery.Node.buildField(EDGES, null, null, { plural: true });
var EMPTY = '';
var ID = 'id';
var IGNORED_KEYS = _defineProperty({
error: true
}, CLIENT_MUTATION_ID, true);
var STUB_CURSOR_ID = 'client:cursor';
/**
* @internal
*
* Applies the results of an update operation (mutation/subscription) to the
* store.
*/
function writeRelayUpdatePayload(writer, operation, payload, _ref) {
var configs = _ref.configs;
var isOptimisticUpdate = _ref.isOptimisticUpdate;
configs.forEach(function (config) {
switch (config.type) {
case RelayMutationType.NODE_DELETE:
handleNodeDelete(writer, payload, config);
break;
case RelayMutationType.RANGE_ADD:
handleRangeAdd(writer, payload, operation, config, isOptimisticUpdate);
break;
case RelayMutationType.RANGE_DELETE:
handleRangeDelete(writer, payload, config);
break;
case RelayMutationType.FIELDS_CHANGE:
case RelayMutationType.REQUIRED_CHILDREN:
break;
default:
console.error('Expected a valid mutation handler type, got `%s`.', config.type);
}
});
handleMerge(writer, payload, operation);
}
/**
* Handles the payload for a node deletion mutation, reading the ID of the node
* to delete from the payload based on the config and then deleting references
* to the node.
*/
function handleNodeDelete(writer, payload, config) {
var recordIDs = payload[config.deletedIDFieldName];
if (!recordIDs) {
// for some mutations, deletions don't always occur so if there's no field
// in the payload, carry on
return;
}
if (Array.isArray(recordIDs)) {
recordIDs.forEach(function (id) {
deleteRecord(writer, id);
});
} else {
deleteRecord(writer, recordIDs);
}
}
/**
* Deletes the record from the store, also removing any references to the node
* from any ranges that contain it (along with the containing edges).
*/
function deleteRecord(writer, recordID) {
var store = writer.getRecordStore();
// skip if already deleted
var status = store.getRecordStatus(recordID);
if (status === RelayRecordStatus.NONEXISTENT) {
return;
}
// Delete the node from any ranges it may be a part of
var connectionIDs = store.getConnectionIDsForRecord(recordID);
if (connectionIDs) {
connectionIDs.forEach(function (connectionID) {
var edgeID = generateClientEdgeID(connectionID, recordID);
store.applyRangeUpdate(connectionID, edgeID, REMOVE);
writer.recordUpdate(edgeID);
writer.recordUpdate(connectionID);
// edges are never nodes, so this will not infinitely recurse
deleteRecord(writer, edgeID);
});
}
// delete the node
store.deleteRecord(recordID);
writer.recordUpdate(recordID);
}
/**
* Handles merging the results of the mutation/subscription into the store,
* updating each top-level field in the data according the fetched
* fields/fragments.
*/
function handleMerge(writer, payload, operation) {
var store = writer.getRecordStore();
// because optimistic payloads may not contain all fields, we loop over
// the data that is present and then have to recurse the query to find
// the matching fields.
//
// TODO #7167718: more efficient mutation/subscription writes
for (var fieldName in payload) {
if (!payload.hasOwnProperty(fieldName)) {
continue;
}
var payloadData = payload[fieldName];
if (payloadData == null || typeof payloadData !== 'object') {
continue;
}
// if the field is an argument-less root call, determine the corresponding
// root record ID
var rootID = store.getRootCallID(fieldName, EMPTY);
// check for valid data (has an ID or is an array) and write the field
if (ID in payloadData || rootID || Array.isArray(payloadData)) {
mergeField(writer, fieldName, payloadData, // checked above: != null and typeof object
operation);
}
}
}
/**
* Merges the results of a single top-level field into the store.
*/
function mergeField(writer, fieldName, payload, operation) {
// don't write mutation/subscription metadata fields
if (fieldName in IGNORED_KEYS) {
return;
}
if (Array.isArray(payload)) {
payload.forEach(function (item) {
if (item[ID]) {
mergeField(writer, fieldName, item, operation);
}
});
return;
}
var store = writer.getRecordStore();
var recordID = payload[ID];
var path;
if (recordID) {
path = new RelayQueryPath(RelayQuery.Node.buildRoot(RelayNodeInterface.NODE, recordID, null, { rootArg: RelayNodeInterface.ID }));
} else {
recordID = store.getRootCallID(fieldName, EMPTY);
// Root fields that do not accept arguments
path = new RelayQueryPath(RelayQuery.Node.buildRoot(fieldName));
}
!recordID ? process.env.NODE_ENV !== 'production' ? invariant(false, 'writeRelayUpdatePayload(): Expected a record ID in the response payload ' + 'supplied to update the store.') : invariant(false) : undefined;
// write the results for only the current field, for every instance of that
// field in any subfield/fragment in the query.
var handleNode = function handleNode(node) {
node.getChildren().forEach(function (child) {
if (child instanceof RelayQuery.Fragment) {
handleNode(child);
} else if (child instanceof RelayQuery.Field && child.getSerializationKey() === fieldName) {
// for flow: types are lost in closures
if (path && recordID) {
// ensure the record exists and then update it
writer.createRecordIfMissing(child, recordID, path);
writer.writePayload(child, recordID, payload, path);
}
}
});
};
handleNode(operation);
}
/**
* Handles the payload for a range addition. The configuration specifies:
* - which field in the payload contains data for the new edge
* - the list of fetched ranges to which the edge should be added
* - whether to append/prepend to each of those ranges
*/
function handleRangeAdd(writer, payload, operation, config, isOptimisticUpdate) {
var clientMutationID = payload[CLIENT_MUTATION_ID];
var store = writer.getRecordStore();
// Extracts the new edge from the payload
var edge = payload[config.edgeName];
if (!edge || !edge.node) {
process.env.NODE_ENV !== 'production' ? warning(false, 'writeRelayUpdatePayload(): Expected response payload to include the ' + 'newly created edge `%s` and its `node` field. Did you forget to ' + 'update the `RANGE_ADD` mutation config?', config.edgeName) : undefined;
return;
}
// Extract the id of the node with the connection that we are adding to.
var connectionParentID = config.parentID || edge.source && edge.source.id;
!connectionParentID ? process.env.NODE_ENV !== 'production' ? invariant(false, 'writeRelayUpdatePayload(): Cannot insert edge without a configured ' + '`parentID` or a `%s.source.id` field.', config.edgeName) : invariant(false) : undefined;
var nodeID = edge.node.id || generateClientID();
var cursor = edge.cursor || STUB_CURSOR_ID;
var edgeData = _extends({}, edge, {
cursor: cursor,
node: _extends({}, edge.node, {
id: nodeID
})
});
// add the node to every connection for this field
var connectionIDs = store.getConnectionIDsForField(connectionParentID, config.connectionName);
if (connectionIDs) {
connectionIDs.forEach(function (connectionID) {
return addRangeNode(writer, operation, config, connectionID, nodeID, edgeData);
});
}
if (isOptimisticUpdate) {
// optimistic updates need to record the generated client ID for
// a to-be-created node
RelayMutationTracker.putClientIDForMutation(nodeID, clientMutationID);
} else {
// non-optimistic updates check for the existence of a generated client
// ID (from the above `if` clause) and link the client ID to the actual
// server ID.
var clientNodeID = RelayMutationTracker.getClientIDForMutation(clientMutationID);
if (clientNodeID) {
RelayMutationTracker.updateClientServerIDMap(clientNodeID, nodeID);
RelayMutationTracker.deleteClientIDForMutation(clientMutationID);
}
}
}
/**
* Writes the node data for the given field to the store and prepends/appends
* the node to the given connection.
*/
function addRangeNode(writer, operation, config, connectionID, nodeID, edgeData) {
var store = writer.getRecordStore();
var filterCalls = store.getRangeFilterCalls(connectionID);
var rangeBehavior = filterCalls ? getRangeBehavior(config.rangeBehaviors, filterCalls) : null;
// no range behavior specified for this combination of filter calls
if (!rangeBehavior) {
return;
}
var edgeID = generateClientEdgeID(connectionID, nodeID);
var path = store.getPathToRecord(connectionID);
!path ? process.env.NODE_ENV !== 'production' ? invariant(false, 'writeRelayUpdatePayload(): Expected a path for connection record, `%s`.', connectionID) : invariant(false) : undefined;
path = path.getPath(EDGES_FIELD, edgeID);
// create the edge record
writer.createRecordIfMissing(EDGES_FIELD, edgeID, path);
// write data for all `edges` fields
// TODO #7167718: more efficient mutation/subscription writes
var hasEdgeField = false;
var handleNode = function handleNode(node) {
node.getChildren().forEach(function (child) {
if (child instanceof RelayQuery.Fragment) {
handleNode(child);
} else if (child instanceof RelayQuery.Field && child.getSchemaName() === config.edgeName) {
hasEdgeField = true;
if (path) {
writer.writePayload(child, edgeID, edgeData, path);
}
}
});
};
handleNode(operation);
!hasEdgeField ? process.env.NODE_ENV !== 'production' ? invariant(false, 'writeRelayUpdatePayload(): Expected mutation query to include the ' + 'relevant edge field, `%s`.', config.edgeName) : invariant(false) : undefined;
// append/prepend the item to the range.
if (rangeBehavior in GraphQLMutatorConstants.RANGE_OPERATIONS) {
store.applyRangeUpdate(connectionID, edgeID, rangeBehavior);
if (writer.hasChangeToRecord(edgeID)) {
writer.recordUpdate(connectionID);
}
} else {
console.error('writeRelayUpdatePayload(): invalid range operation `%s`, valid ' + 'options are `%s` or `%s`.', rangeBehavior, APPEND, PREPEND);
}
}
/**
* Handles the payload for a range edge deletion, which removes the edge from
* a specified range but does not delete the node for that edge. The config
* specifies the path within the payload that contains the connection ID.
*/
function handleRangeDelete(writer, payload, config) {
var recordID = payload[config.deletedIDFieldName];
!(recordID !== undefined) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'writeRelayUpdatePayload(): Missing ID for deleted record at field `%s`.', config.deletedIDFieldName) : invariant(false) : undefined;
// Extract the id of the node with the connection that we are deleting from.
var store = writer.getRecordStore();
var connectionName = config.pathToConnection.pop();
var connectionParentID = getIDFromPath(store, config.pathToConnection, payload);
// Restore pathToConnection to its original state
config.pathToConnection.push(connectionName);
if (!connectionParentID) {
return;
}
var connectionIDs = store.getConnectionIDsForField(connectionParentID, connectionName);
if (connectionIDs) {
connectionIDs.forEach(function (connectionID) {
deleteRangeEdge(writer, connectionID, recordID);
});
}
}
/**
* Removes an edge from a connection without modifying the node data.
*/
function deleteRangeEdge(writer, connectionID, nodeID) {
var store = writer.getRecordStore();
var edgeID = generateClientEdgeID(connectionID, nodeID);
store.applyRangeUpdate(connectionID, edgeID, REMOVE);
deleteRecord(writer, edgeID);
if (writer.hasChangeToRecord(edgeID)) {
writer.recordUpdate(connectionID);
}
}
/**
* 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, calls) {
var call = calls.map(printRelayQueryCall).join('').slice(1);
return rangeBehaviors[call] || null;
}
/**
* Given a payload of data and a path of fields, extracts the `id` of the node
* specified by the path.
*
* Example:
* path: ['root', 'field']
* data: {root: {field: {id: 'xyz'}}}
*
* Returns:
* 'xyz'
*/
function getIDFromPath(store, path, payload) {
// We have a special case for the path for root nodes without ids like
// ['viewer']. We try to match it up with something in the root call mapping
// first.
if (path.length === 1) {
var rootCallID = store.getRootCallID(path[0], EMPTY);
if (rootCallID) {
return rootCallID;
}
}
for (var ii = 0; ii < path.length; ii++) {
var step = path[ii];
if (!payload || typeof payload !== 'object') {
return null;
}
payload = payload[step]; // $FlowIssue: `payload` is an object
}
if (payload && typeof payload === 'object') {
return payload.id;
}
return null;
}
module.exports = RelayProfiler.instrument('writeRelayUpdatePayload', writeRelayUpdatePayload);
/* $FlowIssue #7728187 - Computed Property */