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 / diffRelayQuery.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 diffRelayQuery
 * 
 * @typechecks
 */

'use strict';

var _classCallCheck = require('babel-runtime/helpers/class-call-check')['default'];

var GraphQLStoreDataHandler = require('./GraphQLStoreDataHandler');
var RelayConnectionInterface = require('./RelayConnectionInterface');
var RelayNodeInterface = require('./RelayNodeInterface');
var RelayProfiler = require('./RelayProfiler');
var RelayQuery = require('./RelayQuery');
var RelayQueryPath = require('./RelayQueryPath');

var forEachRootCallArg = require('./forEachRootCallArg');
var invariant = require('fbjs/lib/invariant');
var isCompatibleRelayFragmentType = require('./isCompatibleRelayFragmentType');
var warning = require('fbjs/lib/warning');

var ID = RelayNodeInterface.ID;
var NODE_TYPE = RelayNodeInterface.NODE_TYPE;
var TYPENAME = RelayNodeInterface.TYPENAME;
var EDGES = RelayConnectionInterface.EDGES;
var NODE = RelayConnectionInterface.NODE;
var PAGE_INFO = RelayConnectionInterface.PAGE_INFO;

var idField = RelayQuery.Field.build({
  fieldName: ID,
  metadata: {
    isRequisite: true
  },
  type: 'String'
});
var typeField = RelayQuery.Field.build({
  fieldName: TYPENAME,
  metadata: {
    isRequisite: true
  },
  type: 'String'
});
var nodeWithID = RelayQuery.Field.build({
  fieldName: RelayNodeInterface.NODE,
  children: [idField, typeField],
  type: NODE_TYPE
});

/**
 * @internal
 *
 * Computes the difference between the data requested in `root` and the data
 * available in `store`. It returns a minimal set of queries that will fulfill
 * the difference, or an empty array if the query can be resolved locally.
 */
function diffRelayQuery(root, store, tracker) {
  var path = new RelayQueryPath(root);
  var queries = [];

  var visitor = new RelayDiffQueryBuilder(store, tracker);
  var rootIdentifyingArg = root.getIdentifyingArg();
  var rootIdentifyingArgValue = rootIdentifyingArg && rootIdentifyingArg.value || null;
  var isPluralCall = Array.isArray(rootIdentifyingArgValue) && rootIdentifyingArgValue.length > 1;
  var metadata = undefined;
  if (rootIdentifyingArg != null) {
    metadata = {};
    metadata.identifyingArgName = rootIdentifyingArg.name;
    if (rootIdentifyingArg.type != null) {
      metadata.identifyingArgType = rootIdentifyingArg.type;
    }
  }
  var fieldName = root.getFieldName();
  var storageKey = root.getStorageKey();
  forEachRootCallArg(root, function (identifyingArgValue) {
    var nodeRoot;
    if (isPluralCall) {
      !(identifyingArgValue != null) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'diffRelayQuery(): Unexpected null or undefined value in root call ' + 'argument array for query, `%s(...).', fieldName) : invariant(false) : undefined;
      nodeRoot = RelayQuery.Root.build(root.getName(), fieldName, [identifyingArgValue], root.getChildren(), metadata, root.getType());
    } else {
      // Reuse `root` if it only maps to one result.
      nodeRoot = root;
    }

    // The whole query must be fetched if the root dataID is unknown.
    var dataID = store.getDataID(storageKey, identifyingArgValue);
    if (dataID == null) {
      queries.push(nodeRoot);
      return;
    }

    // Diff the current dataID
    var scope = makeScope(dataID);
    var diffOutput = visitor.visit(nodeRoot, path, scope);
    var diffNode = diffOutput ? diffOutput.diffNode : null;
    if (diffNode) {
      !(diffNode instanceof RelayQuery.Root) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'diffRelayQuery(): Expected result to be a root query.') : invariant(false) : undefined;
      queries.push(diffNode);
    }
  });
  return queries.concat(visitor.getSplitQueries());
}

/**
 * @internal
 *
 * A transform for (node + store) -> (diff + tracked queries). It is analagous
 * to `RelayQueryTransform` with the main differences as follows:
 * - there is no `state` (which allowed for passing data up and down the tree).
 * - data is passed down via `scope`, which flows from a parent field down
 *   through intermediary fragments to the nearest child field.
 * - data is passed up via the return type `{diffNode, trackedNode}`, where:
 *   - `diffNode`: subset of the input that could not diffed out
 *   - `trackedNode`: subset of the input that must be tracked
 *
 * The provided `tracker` is updated whenever the traversal of a node results
 * in a `trackedNode` being created. New top-level queries are not returned
 * up the tree, and instead are available via `getSplitQueries()`.
 */

var RelayDiffQueryBuilder = (function () {
  function RelayDiffQueryBuilder(store, tracker) {
    _classCallCheck(this, RelayDiffQueryBuilder);

    this._store = store;
    this._splitQueries = [];
    this._tracker = tracker;
  }

  /**
   * Helper to construct a plain scope for the given `dataID`.
   */

  RelayDiffQueryBuilder.prototype.splitQuery = function splitQuery(root) {
    this._splitQueries.push(root);
  };

  RelayDiffQueryBuilder.prototype.getSplitQueries = function getSplitQueries() {
    return this._splitQueries;
  };

  RelayDiffQueryBuilder.prototype.visit = function visit(node, path, scope) {
    if (node instanceof RelayQuery.Field) {
      return this.visitField(node, path, scope);
    } else if (node instanceof RelayQuery.Fragment) {
      return this.visitFragment(node, path, scope);
    } else if (node instanceof RelayQuery.Root) {
      return this.visitRoot(node, path, scope);
    }
  };

  RelayDiffQueryBuilder.prototype.visitRoot = function visitRoot(node, path, scope) {
    return this.traverse(node, path, scope);
  };

  RelayDiffQueryBuilder.prototype.visitFragment = function visitFragment(node, path, scope) {
    return this.traverse(node, path, scope);
  };

  /**
   * Diffs the field conditionally based on the `scope` from the nearest
   * ancestor field.
   */

  RelayDiffQueryBuilder.prototype.visitField = function visitField(node, path, _ref) {
    var connectionField = _ref.connectionField;
    var dataID = _ref.dataID;
    var edgeID = _ref.edgeID;
    var rangeInfo = _ref.rangeInfo;

    // special case when inside a connection traversal
    if (connectionField && rangeInfo) {
      if (edgeID) {
        // When traversing a specific connection edge only look at `edges`
        if (node.getSchemaName() === EDGES) {
          return this.diffConnectionEdge(connectionField, node, // edge field
          path.getPath(node, edgeID), edgeID, rangeInfo);
        } else {
          return null;
        }
      } else {
        // When traversing connection metadata fields, edges/page_info are
        // only kept if there are range extension calls. Other fields fall
        // through to regular diffing.
        if (node.getSchemaName() === EDGES || node.getSchemaName() === PAGE_INFO) {
          return rangeInfo.diffCalls.length > 0 ? {
            diffNode: node,
            trackedNode: null
          } : null;
        }
      }
    }

    // default field diffing algorithm
    if (node.isScalar()) {
      return this.diffScalar(node, dataID);
    } else if (node.isGenerated()) {
      return {
        diffNode: node,
        trackedNode: null
      };
    } else if (node.isConnection()) {
      return this.diffConnection(node, path, dataID);
    } else if (node.isPlural()) {
      return this.diffPluralLink(node, path, dataID);
    } else {
      return this.diffLink(node, path, dataID);
    }
  };

  /**
   * Visit all the children of the given `node` and merge their results.
   */

  RelayDiffQueryBuilder.prototype.traverse = function traverse(node, path, scope) {
    var _this = this;

    var diffNode = undefined;
    var diffChildren = undefined;
    var trackedNode = undefined;
    var trackedChildren = undefined;
    var hasDiffField = false;
    var hasTrackedField = false;

    node.getChildren().forEach(function (child) {
      if (child instanceof RelayQuery.Field) {
        var diffOutput = _this.visitField(child, path, scope);
        var diffChild = diffOutput ? diffOutput.diffNode : null;
        var trackedChild = diffOutput ? diffOutput.trackedNode : null;

        // Diff uses child nodes and keeps requisite fields
        if (diffChild) {
          diffChildren = diffChildren || [];
          diffChildren.push(diffChild);
          hasDiffField = hasDiffField || !diffChild.isGenerated();
        } else if (child.isRequisite() && !scope.rangeInfo) {
          // The presence of `rangeInfo` indicates that we are traversing
          // connection metadata fields, in which case `visitField` will ensure
          // that `edges` and `page_info` are kept when necessary. The requisite
          // check alone could cause these fields to be added back when not
          // needed.
          //
          // Example: `friends.first(3) {count, edges {...}, page_info {...} }
          // If all `edges` were fetched but `count` is unfetched, the diff
          // should be `friends.first(3) {count}` and not include `page_info`.
          diffChildren = diffChildren || [];
          diffChildren.push(child);
        }
        // Tracker uses tracked children and keeps requisite fields
        if (trackedChild) {
          trackedChildren = trackedChildren || [];
          trackedChildren.push(trackedChild);
          hasTrackedField = hasTrackedField || !trackedChild.isGenerated();
        } else if (child.isRequisite()) {
          trackedChildren = trackedChildren || [];
          trackedChildren.push(child);
        }
      } else if (child instanceof RelayQuery.Fragment) {
        var isCompatibleType = isCompatibleRelayFragmentType(child, _this._store.getType(scope.dataID));
        if (isCompatibleType) {
          var diffOutput = _this.traverse(child, path, scope);
          var diffChild = diffOutput ? diffOutput.diffNode : null;
          var trackedChild = diffOutput ? diffOutput.trackedNode : null;

          if (diffChild) {
            diffChildren = diffChildren || [];
            diffChildren.push(diffChild);
            hasDiffField = true;
          }
          if (trackedChild) {
            trackedChildren = trackedChildren || [];
            trackedChildren.push(trackedChild);
            hasTrackedField = true;
          }
        } else {
          // Non-matching fragment types are similar to requisite fields:
          // they don't need to be diffed against and should only be included
          // if something *else* is missing from the node.
          diffChildren = diffChildren || [];
          diffChildren.push(child);
        }
      }
    });

    // Only return diff/tracked node if there are non-generated fields
    if (diffChildren && hasDiffField) {
      diffNode = node.clone(diffChildren);
    }
    if (trackedChildren && hasTrackedField) {
      trackedNode = node.clone(trackedChildren);
    }
    // Record tracked nodes. Fragments can be skipped because these will
    // always be composed into, and therefore tracked by, their nearest
    // non-fragment parent.
    if (trackedNode && !(trackedNode instanceof RelayQuery.Fragment)) {
      this._tracker.trackNodeForID(trackedNode, scope.dataID, path);
    }

    return {
      diffNode: diffNode,
      trackedNode: trackedNode
    };
  };

  /**
   * Diff a scalar field such as `name` or `id`.
   */

  RelayDiffQueryBuilder.prototype.diffScalar = function diffScalar(field, dataID) {
    if (this._store.getField(dataID, field.getStorageKey()) === undefined) {
      return {
        diffNode: field,
        trackedNode: null
      };
    }
    return null;
  };

  /**
   * Diff a field-of-fields such as `profile_picture {...}`. Returns early if
   * the field has not been fetched, otherwise the result of traversal.
   */

  RelayDiffQueryBuilder.prototype.diffLink = function diffLink(field, path, dataID) {
    var nextDataID = this._store.getLinkedRecordID(dataID, field.getStorageKey());
    if (nextDataID === undefined) {
      return {
        diffNode: field,
        trackedNode: null
      };
    }
    if (nextDataID === null) {
      return null;
    }

    return this.traverse(field, path.getPath(field, nextDataID), makeScope(nextDataID));
  };

  /**
   * Diffs a non-connection plural field against each of the fetched items.
   * Note that scalar plural fields are handled by `_diffScalar`.
   */

  RelayDiffQueryBuilder.prototype.diffPluralLink = function diffPluralLink(field, path, dataID) {
    var _this2 = this;

    var linkedIDs = this._store.getLinkedRecordIDs(dataID, field.getStorageKey());
    if (linkedIDs === undefined) {
      // not fetched
      return {
        diffNode: field,
        trackedNode: null
      };
    } else if (linkedIDs === null || linkedIDs.length === 0) {
      // empty array means nothing to fetch
      return null;
    } else if (field.getInferredRootCallName() === NODE) {
      // The items in this array are fetchable and may have been filled in
      // from other sources, so check them all. For example, `Story{actors}`
      // is an array (but not a range), and the Actors in that array likely
      // had data fetched for them elsewhere (like `viewer(){actor}`).
      var hasSplitQueries = false;
      linkedIDs.forEach(function (itemID) {
        var itemState = _this2.traverse(field, path.getPath(field, itemID), makeScope(itemID));
        if (itemState) {
          // If any child was tracked then `field` will also be tracked
          hasSplitQueries = hasSplitQueries || !!itemState.trackedNode || !!itemState.diffNode;
          // split diff nodes into root queries
          if (itemState.diffNode) {
            _this2.splitQuery(buildRoot(itemID, itemState.diffNode.getChildren(), path.getName(), field.getType()));
          }
        }
      });
      // if sub-queries are split then this *entire* field will be tracked,
      // therefore we don't need to merge the `trackedNode` from each item
      if (hasSplitQueries) {
        return {
          diffNode: null,
          trackedNode: field
        };
      }
    } else {
      // The items in this array are not fetchable by ID, so nothing else
      // could have fetched additional data for individual items. Therefore,
      // we only need to diff the first record to figure out which fields have
      // previously been fetched.
      var sampleItemID = linkedIDs[0];
      return this.traverse(field, path.getPath(field, sampleItemID), makeScope(sampleItemID));
    }
    return null;
  };

  /**
   * Diff a connection field such as `news_feed.first(3)`. Returns early if
   * the range has not been fetched or the entire range has already been
   * fetched. Otherwise the diff output is a clone of `field` with updated
   * after/first and before/last calls.
   */

  RelayDiffQueryBuilder.prototype.diffConnection = function diffConnection(field, path, dataID) {
    var _this3 = this;

    var store = this._store;
    var connectionID = store.getLinkedRecordID(dataID, field.getStorageKey());
    var rangeInfo = store.getRangeMetadata(connectionID, field.getCallsWithValues());
    // Keep the field if the connection is unfetched
    if (connectionID === undefined) {
      return {
        diffNode: field,
        trackedNode: null
      };
    }
    // Skip if the connection is deleted.
    if (connectionID === null) {
      return null;
    }
    // If metadata fields but not edges are fetched, diff as a normal field.
    // In practice, `rangeInfo` is `undefined` if unfetched, `null` if the
    // connection was deleted (in which case `connectionID` is null too).
    if (rangeInfo == null) {
      return this.traverse(field, path.getPath(field, connectionID), makeScope(connectionID));
    }
    var diffCalls = rangeInfo.diffCalls;
    var filteredEdges = rangeInfo.filteredEdges;

    // check existing edges for missing fields
    var hasSplitQueries = false;
    filteredEdges.forEach(function (edge) {
      // Flow loses type information in closures
      if (rangeInfo && connectionID) {
        var scope = {
          connectionField: field,
          dataID: connectionID,
          edgeID: edge.edgeID,
          rangeInfo: rangeInfo
        };
        var diffOutput = _this3.traverse(field, path.getPath(field, edge.edgeID), scope);
        // If any edges were missing data (resulting in a split query),
        // then the entire original connection field must be tracked.
        if (diffOutput) {
          hasSplitQueries = hasSplitQueries || !!diffOutput.trackedNode;
        }
      }
    });

    // Scope has null `edgeID` to skip looking at `edges` fields.
    var scope = {
      connectionField: field,
      dataID: connectionID,
      edgeID: null,
      rangeInfo: rangeInfo
    };
    // diff non-`edges` fields such as `count`
    var diffOutput = this.traverse(field, path.getPath(field, connectionID), scope);
    var diffNode = diffOutput ? diffOutput.diffNode : null;
    var trackedNode = diffOutput ? diffOutput.trackedNode : null;
    if (diffCalls.length && diffNode instanceof RelayQuery.Field) {
      diffNode = diffNode.cloneFieldWithCalls(diffNode.getChildren(), diffCalls);
    }
    // if a sub-query was split, then we must track the entire field, which will
    // be a superset of the `trackedNode` from traversing any metadata fields.
    // Example:
    // dataID: `4`
    // node: `friends.first(3)`
    // diffNode: null
    // splitQueries: `node(friend1) {...}`, `node(friend2) {...}`
    //
    // In this case the two fetched `node` queries do not reflect the fact that
    // `friends.first(3)` were fetched for item `4`, so `friends.first(3)` has
    // to be tracked as-is.
    if (hasSplitQueries) {
      trackedNode = field;
    }

    return {
      diffNode: diffNode,
      trackedNode: trackedNode
    };
  };

  /**
   * Diff an `edges` field for the edge rooted at `edgeID`, splitting a new
   * root query to fetch any missing data (via a `node(id)` root if the
   * field is refetchable or a `...{connection.find(id){}}` query if the
   * field is not refetchable).
   */

  RelayDiffQueryBuilder.prototype.diffConnectionEdge = function diffConnectionEdge(connectionField, edgeField, path, edgeID, rangeInfo) {
    var nodeID = this._store.getLinkedRecordID(edgeID, NODE);
    if (!nodeID || GraphQLStoreDataHandler.isClientID(nodeID)) {
      process.env.NODE_ENV !== 'production' ? warning(false, 'RelayDiffQueryBuilder: connection `node{*}` can only be refetched ' + 'if the node is refetchable by `id`. Cannot refetch data for field ' + '`%s`.', connectionField.getStorageKey()) : undefined;
      return null;
    }

    var hasSplitQueries = false;
    var diffOutput = this.traverse(edgeField, path.getPath(edgeField, edgeID), makeScope(edgeID));
    var diffNode = diffOutput ? diffOutput.diffNode : null;
    var trackedNode = diffOutput ? diffOutput.trackedNode : null;

    if (diffNode) {
      var _splitNodeAndEdgesFields = splitNodeAndEdgesFields(diffNode);

      var diffEdgesField = _splitNodeAndEdgesFields.edges;
      var diffNodeField = _splitNodeAndEdgesFields.node;

      // split missing `node` fields into a `node(id)` root query
      if (diffNodeField) {
        hasSplitQueries = true;
        var nodeField = edgeField.getFieldByStorageKey('node');
        !nodeField ? process.env.NODE_ENV !== 'production' ? invariant(false, 'RelayDiffQueryBuilder: expected a `node` field for connection `%s`.', connectionField.getSchemaName()) : invariant(false) : undefined;
        this.splitQuery(buildRoot(nodeID, diffNodeField.getChildren(), path.getName(), nodeField.getType()));
      }

      // split missing `edges` fields into a `connection.find(id)` query
      // if `find` is supported, otherwise warn
      if (diffEdgesField) {
        if (connectionField.isFindable()) {
          diffEdgesField = diffEdgesField.clone(diffEdgesField.getChildren().concat(nodeWithID));
          var connectionFind = connectionField.cloneFieldWithCalls([diffEdgesField], rangeInfo.filterCalls.concat({ name: 'find', value: nodeID }));
          if (connectionFind) {
            hasSplitQueries = true;
            // current path has `parent`, `connection`, `edges`; pop to parent
            var connectionParent = path.getParent().getParent();
            this.splitQuery(connectionParent.getQuery(connectionFind));
          }
        } else {
          process.env.NODE_ENV !== 'production' ? warning(false, 'RelayDiffQueryBuilder: connection `edges{*}` fields can only be ' + 'refetched if the connection supports the `find` call. Cannot ' + 'refetch data for field `%s`.', connectionField.getStorageKey()) : undefined;
        }
      }
    }

    // Connection edges will never return diff nodes; instead missing fields
    // are fetched by new root queries. Tracked nodes are returned if either
    // a child field was tracked or missing fields were split into a new query.
    // The returned `trackedNode` is never tracked directly: instead it serves
    // as an indicator to `diffConnection` that the entire connection field must
    // be tracked.
    return {
      diffNode: null,
      trackedNode: hasSplitQueries ? edgeField : trackedNode
    };
  };

  return RelayDiffQueryBuilder;
})();

function makeScope(dataID) {
  return {
    connectionField: null,
    dataID: dataID,
    edgeID: null,
    rangeInfo: null
  };
}

/**
 * Returns a clone of the input with `edges` and `node` sub-fields split into
 * separate `edges` and `node` roots. Example:
 *
 * Input:
 * edges {
 *   edge_field,
 *   node {
 *     a,
 *     b
 *   },
 *   ${
 *     Fragment {
 *       edge_field_2,
 *       node {
 *         c
 *       }
 *     }
 *   }
 * }
 *
 * Output:
 * node:
 *   edges {
 *     a,      // flattened
 *     b,      // flattend
 *     ${
 *       Fragment {
 *         c  // flattened
 *       }
 *     }
 *   }
 * edges:
 *   edges {
 *     edge_field,
 *     ${
 *       Fragment {
 *         edge_field_2
 *       }
 *     }
 *   }
 */
function splitNodeAndEdgesFields(edgeOrFragment) {
  var children = edgeOrFragment.getChildren();
  var edgeChildren = [];
  var hasNodeChild = false;
  var nodeChildren = [];
  var hasEdgeChild = false;
  for (var ii = 0; ii < children.length; ii++) {
    var child = children[ii];
    if (child instanceof RelayQuery.Field) {
      if (child.getSchemaName() === NODE) {
        var subFields = child.getChildren();
        nodeChildren = nodeChildren.concat(subFields);
        // can skip if `node` only has an `id` field
        hasNodeChild = hasNodeChild || subFields.length !== 1 || !(subFields[0] instanceof RelayQuery.Field) ||
        /* $FlowFixMe(>=0.13.0) - subFields[0] needs to be in a local for Flow to
         * narrow its type, otherwise Flow thinks its a RelayQueryNode without
         * method `getSchemaName`
         */
        subFields[0].getSchemaName() !== 'id';
      } else {
        edgeChildren.push(child);
        hasEdgeChild = hasEdgeChild || !child.isRequisite();
      }
    } else if (child instanceof RelayQuery.Fragment) {
      var _splitNodeAndEdgesFields2 = splitNodeAndEdgesFields(child);

      var edges = _splitNodeAndEdgesFields2.edges;
      var node = _splitNodeAndEdgesFields2.node;

      if (edges) {
        edgeChildren.push(edges);
        hasEdgeChild = true;
      }
      if (node) {
        nodeChildren.push(node);
        hasNodeChild = true;
      }
    }
  }
  return {
    edges: hasEdgeChild ? edgeOrFragment.clone(edgeChildren) : null,
    node: hasNodeChild ? edgeOrFragment.clone(nodeChildren) : null
  };
}

function buildRoot(rootID, nodes, name, type) {
  var children = [idField, typeField];
  var fields = [];
  nodes.forEach(function (node) {
    if (node instanceof RelayQuery.Field) {
      fields.push(node);
    } else {
      children.push(node);
    }
  });
  children.push(RelayQuery.Fragment.build('diffRelayQuery', type, fields));

  return RelayQuery.Root.build(name, NODE, rootID, children, { identifyingArgName: RelayNodeInterface.ID }, NODE_TYPE);
}

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