Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Bower components Debian packages RPM packages NuGet packages

pfchangs / react-relay   js

Repository URL to install this package:

Version: 0.7.1-ccinternal 

/ lib / writeRelayUpdatePayload.js

/**
 * 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 RelayRecordState = require('./RelayRecordState');

var generateClientEdgeID = require('./generateClientEdgeID');
var generateClientID = require('./generateClientID');
var invariant = require('fbjs/lib/invariant');
var serializeRelayQueryCall = require('./serializeRelayQueryCall');
var warning = require('fbjs/lib/warning');

// TODO: Replace with enumeration for possible config types.
/* OperationConfig was originally typed such that each property had the type
 * mixed.  Mixed is safer than any, but that safety comes from Flow forcing you
 * to inspect a mixed value at runtime before using it.  However these mixeds
 * are ending up everywhere and are not being inspected */
var CLIENT_MUTATION_ID = RelayConnectionInterface.CLIENT_MUTATION_ID;
var EDGES = RelayConnectionInterface.EDGES;
var ANY_TYPE = RelayNodeInterface.ANY_TYPE;
var ID = RelayNodeInterface.ID;
var NODE = RelayNodeInterface.NODE;
var NODE_TYPE = RelayNodeInterface.NODE_TYPE;
var APPEND = GraphQLMutatorConstants.APPEND;
var PREPEND = GraphQLMutatorConstants.PREPEND;
var REMOVE = GraphQLMutatorConstants.REMOVE;

var EDGES_FIELD = RelayQuery.Field.build({
  fieldName: EDGES,
  type: ANY_TYPE,
  metadata: { isPlural: true }
});
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.getRecordState(recordID);
  if (status === RelayRecordState.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]; // #9357395
    if (typeof payloadData !== 'object' || payloadData == null) {
      continue;
    }
    // if the field is an argument-less root call, determine the corresponding
    // root record ID
    var rootID = store.getDataID(fieldName);
    // 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, 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 (typeof item === 'object' && item != null && !Array.isArray(item)) {
        if (getString(item, ID)) {
          mergeField(writer, fieldName, item, operation);
        }
      }
    });
    return;
  }
  // reassign to preserve type information in below closure
  var payloadData = payload;

  var store = writer.getRecordStore();
  var recordID = getString(payloadData, ID);
  var path = undefined;

  if (recordID != null) {
    path = new RelayQueryPath(RelayQuery.Root.build('writeRelayUpdatePayload', NODE, recordID, null, { identifyingArgName: ID }, NODE_TYPE));
  } else {
    recordID = store.getDataID(fieldName);
    // Root fields that do not accept arguments
    path = new RelayQueryPath(RelayQuery.Root.build('writeRelayUpdatePayload', fieldName, null, null, null, ANY_TYPE));
  }
  !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) {
          var typeName = writer.getRecordTypeName(child, recordID, payloadData);
          // ensure the record exists and then update it
          writer.createRecordIfMissing(child, recordID, typeName, path);
          writer.writePayload(child, recordID, payloadData, 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 = getString(payload, CLIENT_MUTATION_ID);
  !clientMutationID ? process.env.NODE_ENV !== 'production' ? invariant(false, 'writeRelayUpdatePayload(): Expected operation `%s` to have a `%s`.', operation.getName(), CLIENT_MUTATION_ID) : invariant(false) : undefined;
  var store = writer.getRecordStore();

  // Extracts the new edge from the payload
  var edge = getObject(payload, config.edgeName);
  var edgeNode = edge && getObject(edge, NODE);
  if (!edge || !edgeNode) {
    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;
  if (!connectionParentID) {
    var edgeSource = getObject(edge, 'source');
    if (edgeSource) {
      connectionParentID = getString(edgeSource, 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 = getString(edgeNode, ID) || generateClientID();
  var cursor = edge.cursor || STUB_CURSOR_ID;
  var edgeData = _extends({}, edge, {
    cursor: cursor,
    node: _extends({}, edgeNode, {
      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
  var typeName = writer.getRecordTypeName(EDGES_FIELD, edgeID, edgeData);
  writer.createRecordIfMissing(EDGES_FIELD, edgeID, typeName, 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 maybeRecordID = getString(payload, config.deletedIDFieldName);
  !(maybeRecordID != null) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'writeRelayUpdatePayload(): Missing ID for deleted record at field `%s`.', config.deletedIDFieldName) : invariant(false) : undefined;
  var recordID = maybeRecordID; // Flow loses type refinements in closures

  // 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(serializeRelayQueryCall).sort().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.getDataID(path[0]);
    if (rootCallID) {
      return rootCallID;
    }
  }
  var payloadItem = path.reduce(function (payloadItem, step) {
    return payloadItem ? getObject(payloadItem, step) : null;
  }, payload);
  if (payloadItem) {
    var id = getString(payloadItem, ID);
    !(id != null) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'writeRelayUpdatePayload(): Expected `%s.id` to be a string.', path.join('.')) : invariant(false) : undefined;
    return id;
  }
  return null;
}

function getString(payload, field) {
  var value = payload[field];
  // Coerce numbers to strings for backwards compatibility.
  if (typeof value === 'number') {
    process.env.NODE_ENV !== 'production' ? warning(false, 'writeRelayUpdatePayload(): Expected `%s` to be a string, got the ' + 'number `%s`.', field, value) : undefined;
    value = '' + value;
  }
  !(value == null || typeof value === 'string') ? process.env.NODE_ENV !== 'production' ? invariant(false, 'writeRelayUpdatePayload(): Expected `%s` to be a string, got `%s`.', field, JSON.stringify(value)) : invariant(false) : undefined;
  return value;
}

function getObject(payload, field) {
  var value = payload[field];
  !(value == null || typeof value === 'object' && !Array.isArray(value)) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'writeRelayUpdatePayload(): Expected `%s` to be an object, got `%s`.', field, JSON.stringify(value)) : invariant(false) : undefined;
  return value;
}

module.exports = RelayProfiler.instrument('writeRelayUpdatePayload', writeRelayUpdatePayload);