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

'use strict';

var _inherits = require('babel-runtime/helpers/inherits')['default'];

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

var _extends = require('babel-runtime/helpers/extends')['default'];

var RelayQuery = require('./RelayQuery');

var RelayConnectionInterface = require('./RelayConnectionInterface');
var RelayNodeInterface = require('./RelayNodeInterface');

var RelayQueryVisitor = require('./RelayQueryVisitor');
var RelayRecordState = require('./RelayRecordState');

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

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

/**
 * @internal
 *
 * Helper for writing the result of one or more queries/operations into the
 * store, updating tracked queries, and recording changed record IDs.
 */

var RelayQueryWriter = (function (_RelayQueryVisitor) {
  _inherits(RelayQueryWriter, _RelayQueryVisitor);

  function RelayQueryWriter(store, queryTracker, changeTracker, options) {
    _classCallCheck(this, RelayQueryWriter);

    _RelayQueryVisitor.call(this);
    this._changeTracker = changeTracker;
    this._forceIndex = options && options.forceIndex ? options.forceIndex : 0;
    this._isOptimisticUpdate = !!(options && options.isOptimisticUpdate);
    this._store = store;
    this._queryTracker = queryTracker;
    this._updateTrackedQueries = !!(options && options.updateTrackedQueries);
  }

  RelayQueryWriter.prototype.getRecordStore = function getRecordStore() {
    return this._store;
  };

  RelayQueryWriter.prototype.getRecordTypeName = function getRecordTypeName(node, recordID, payload) {
    if (this._isOptimisticUpdate) {
      // Optimistic queries are inferred and fields have a generic 'any' type.
      return null;
    }
    var typeName = payload[TYPENAME];
    if (typeName == null && !node.isAbstract()) {
      typeName = node.getType();
    }
    process.env.NODE_ENV !== 'production' ? warning(typeName && typeName !== ANY_TYPE, 'RelayQueryWriter: Could not find a type name for record `%s`.', recordID) : undefined;
    return typeName || null;
  };

  /**
   * Traverses a query and payload in parallel, writing the results into the
   * store.
   */

  RelayQueryWriter.prototype.writePayload = function writePayload(node, recordID, responseData, path) {
    var _this = this;

    var state = {
      nodeID: null,
      path: path,
      recordID: recordID,
      responseData: responseData
    };

    if (node instanceof RelayQuery.Field && !node.isScalar()) {
      // for non-scalar fields, the recordID is the parent
      node.getChildren().forEach(function (child) {
        _this.visit(child, state);
      });
      return;
    }

    this.visit(node, state);
  };

  /**
   * Records are "created" whenever an entry did not previously exist for the
   * `recordID`, including cases when a `recordID` is created with a null value.
   */

  RelayQueryWriter.prototype.recordCreate = function recordCreate(recordID) {
    this._changeTracker.createID(recordID);
  };

  /**
   * Records are "updated" if any field changes (including being set to null).
   * Updates are not recorded for newly created records.
   */

  RelayQueryWriter.prototype.recordUpdate = function recordUpdate(recordID) {
    this._changeTracker.updateID(recordID);
  };

  /**
   * Determine if the record was created or updated by this write operation.
   */

  RelayQueryWriter.prototype.hasChangeToRecord = function hasChangeToRecord(recordID) {
    return this._changeTracker.hasChange(recordID);
  };

  /**
   * Determine if the record was created by this write operation.
   */

  RelayQueryWriter.prototype.isNewRecord = function isNewRecord(recordID) {
    return this._changeTracker.isNewRecord(recordID);
  };

  /**
   * Helper to create a record and the corresponding notification.
   */

  RelayQueryWriter.prototype.createRecordIfMissing = function createRecordIfMissing(node, recordID, typeName, path) {
    var recordState = this._store.getRecordState(recordID);
    if (recordState !== RelayRecordState.EXISTENT) {
      this._store.putRecord(recordID, typeName, path);
      this.recordCreate(recordID);
    }
    if (this.isNewRecord(recordID) || this._updateTrackedQueries) {
      this._queryTracker.trackNodeForID(node, recordID, path);
    }
  };

  RelayQueryWriter.prototype.visitRoot = function visitRoot(root, state) {
    var path = state.path;
    var recordID = state.recordID;
    var responseData = state.responseData;

    var recordState = this._store.getRecordState(recordID);

    // GraphQL should never return undefined for a field
    if (responseData == null) {
      !(responseData !== undefined) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'RelayQueryWriter: Unexpectedly encountered `undefined` in payload. ' + 'Cannot set root record `%s` to undefined.', recordID) : invariant(false) : undefined;
      this._store.deleteRecord(recordID);
      if (recordState === RelayRecordState.EXISTENT) {
        this.recordUpdate(recordID);
      }
      return;
    }
    !(typeof responseData === 'object' && responseData !== null) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'RelayQueryWriter: Cannot update record `%s`, expected response to be ' + 'an array or object.', recordID) : invariant(false) : undefined;
    if (recordState !== RelayRecordState.EXISTENT) {
      var typeName = this.getRecordTypeName(root, recordID, responseData);
      this._store.putRecord(recordID, typeName, path);
      this.recordCreate(recordID);
    }
    if (this.isNewRecord(recordID) || this._updateTrackedQueries) {
      this._queryTracker.trackNodeForID(root, recordID, path);
    }
    this.traverse(root, state);
  };

  RelayQueryWriter.prototype.visitFragment = function visitFragment(fragment, state) {
    var recordID = state.recordID;

    if (fragment.isDeferred()) {
      this._store.setHasDeferredFragmentData(recordID, fragment.getCompositeHash());
      this.recordUpdate(recordID);
    }
    // Skip fragments that do not match the record's concrete type. Fragments
    // cannot be skipped for optimistic writes because optimistically created
    // records *may* have a default `Node` type.
    if (this._isOptimisticUpdate || isCompatibleRelayFragmentType(fragment, this._store.getType(recordID))) {
      var _path = state.path.getPath(fragment, recordID);
      this.traverse(fragment, _extends({}, state, {
        path: _path
      }));
    }
  };

  RelayQueryWriter.prototype.visitField = function visitField(field, state) {
    var recordID = state.recordID;
    var responseData = state.responseData;

    !(this._store.getRecordState(recordID) === RelayRecordState.EXISTENT) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'RelayQueryWriter: Cannot update a non-existent record, `%s`.', recordID) : invariant(false) : undefined;
    !(typeof responseData === 'object' && responseData !== null) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'RelayQueryWriter: Cannot update record `%s`, expected response to be ' + 'an object.', recordID) : invariant(false) : undefined;

    // handle missing data
    var fieldData = responseData[field.getSerializationKey()];
    if (fieldData === undefined) {
      return;
    }
    if (fieldData === null) {
      this._store.deleteField(recordID, field.getStorageKey());
      this.recordUpdate(recordID);
      return;
    }

    if (field.isScalar()) {
      this._writeScalar(field, state, recordID, fieldData);
    } else if (field.isConnection()) {
      this._writeConnection(field, state, recordID, fieldData);
    } else if (field.isPlural()) {
      this._writePluralLink(field, state, recordID, fieldData);
    } else {
      this._writeLink(field, state, recordID, fieldData);
    }
  };

  /**
   * Writes the value for a 'scalar' field such as `id` or `name`. The response
   * data is expected to be scalar values or arrays of scalar values.
   */

  RelayQueryWriter.prototype._writeScalar = function _writeScalar(field, state, recordID, nextValue) {
    var storageKey = field.getStorageKey();
    var prevValue = this._store.getField(recordID, storageKey);

    // always update the store to ensure the value is present in the appropriate
    // data sink (records/queuedRecords), but only record an update if the value
    // changed.
    this._store.putField(recordID, storageKey, nextValue);

    // TODO: Flow: `nextValue` is an array, array indexing should work
    if (Array.isArray(prevValue) && Array.isArray(nextValue) && prevValue.length === nextValue.length && prevValue.every(function (prev, ii) {
      return prev === nextValue[ii];
    })) {
      return;
    } else if (prevValue === nextValue) {
      return;
    }
    this.recordUpdate(recordID);
  };

  /**
   * Writes data for connection fields such as `news_feed` or `friends`. The
   * response data is expected to be array of edge objects.
   */

  RelayQueryWriter.prototype._writeConnection = function _writeConnection(field, state, recordID, connectionData) {
    // Each unique combination of filter calls is stored in its own
    // generated record (ex: `field.orderby(x)` results are separate from
    // `field.orderby(y)` results).
    var storageKey = field.getStorageKey();
    var connectionID = this._store.getLinkedRecordID(recordID, storageKey) || generateClientID();

    var connectionRecordState = this._store.getRecordState(connectionID);
    var hasEdges = !!(field.getFieldByStorageKey(EDGES) || connectionData != null && typeof connectionData === 'object' && connectionData[EDGES]);
    var path = state.path.getPath(field, connectionID);
    // always update the store to ensure the value is present in the appropriate
    // data sink (records/queuedRecords), but only record an update if the value
    // changed.
    this._store.putRecord(connectionID, null, path);
    this._store.putLinkedRecordID(recordID, storageKey, connectionID);
    // record the create/update only if something changed
    if (connectionRecordState !== RelayRecordState.EXISTENT) {
      this.recordUpdate(recordID);
      this.recordCreate(connectionID);
    }
    if (this.isNewRecord(connectionID) || this._updateTrackedQueries) {
      this._queryTracker.trackNodeForID(field, connectionID, path);
    }

    // Only create a range if `edges` field is present
    // Overwrite an existing range only if the new force index is greater
    if (hasEdges && (!this._store.hasRange(connectionID) || this._forceIndex && this._forceIndex > this._store.getRangeForceIndex(connectionID))) {
      this._store.putRange(connectionID, field.getCallsWithValues(), this._forceIndex);
      this.recordUpdate(connectionID);
    }

    var connectionState = {
      nodeID: null,
      path: path,
      recordID: connectionID,
      responseData: connectionData
    };
    this._traverseConnection(field, field, connectionState);
  };

  /**
   * Recurse through connection subfields and write their results. This is
   * necessary because handling an `edges` field also requires information about
   * the parent connection field (see `_writeEdges`).
   */

  RelayQueryWriter.prototype._traverseConnection = function _traverseConnection(connection, // the parent connection
  node, // the parent connection or an intermediary fragment
  state) {
    var _this2 = this;

    node.getChildren().forEach(function (child) {
      if (child instanceof RelayQuery.Field) {
        if (child.getSchemaName() === EDGES) {
          _this2._writeEdges(connection, child, state);
        } else if (child.getSchemaName() !== PAGE_INFO) {
          // Page info is handled by the range
          // Otherwise, write metadata fields normally (ex: `count`)
          _this2.visit(child, state);
        }
      } else {
        // Fragment case, recurse keeping track of parent connection
        _this2._traverseConnection(connection, child, state);
      }
    });
  };

  /**
   * Update a connection with newly fetched edges.
   */

  RelayQueryWriter.prototype._writeEdges = function _writeEdges(connection, edges, state) {
    var _this3 = this;

    var connectionID = state.recordID;
    var connectionData = state.responseData;

    !(typeof connectionData === 'object' && connectionData !== null) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'RelayQueryWriter: Cannot write edges for malformed connection `%s` on ' + 'record `%s`, expected the response to be an object.', connection.getDebugName(), connectionID) : invariant(false) : undefined;
    var edgesData = connectionData[EDGES];

    // Validate response data.
    if (edgesData == null) {
      process.env.NODE_ENV !== 'production' ? warning(false, 'RelayQueryWriter: Cannot write edges for connection `%s` on record ' + '`%s`, expected a response for field `edges`.', connection.getDebugName(), connectionID) : undefined;
      return;
    }
    !Array.isArray(edgesData) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'RelayQueryWriter: Cannot write edges for connection `%s` on record ' + '`%s`, expected `edges` to be an array.', connection.getDebugName(), connectionID) : invariant(false) : undefined;

    var rangeCalls = connection.getCallsWithValues();
    !RelayConnectionInterface.hasRangeCalls(rangeCalls) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'RelayQueryWriter: Cannot write edges for connection `%s` on record ' + '`%s` without `first`, `last`, or `find` argument.', connection.getDebugName(), connectionID) : invariant(false) : undefined;
    var rangeInfo = this._store.getRangeMetadata(connectionID, rangeCalls);
    !rangeInfo ? process.env.NODE_ENV !== 'production' ? invariant(false, 'RelayQueryWriter: Expected a range to exist for connection field `%s` ' + 'on record `%s`.', connection.getDebugName(), connectionID) : invariant(false) : undefined;
    var fetchedEdgeIDs = [];
    var filteredEdges = rangeInfo.filteredEdges;
    var isUpdate = false;
    var nextIndex = 0;
    // Traverse connection edges, reusing existing edges if they exist
    edgesData.forEach(function (edgeData) {
      // validate response data
      if (edgeData == null) {
        return;
      }
      !(typeof edgeData === 'object' && edgeData) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'RelayQueryWriter: Cannot write edge for connection field `%s` on ' + 'record `%s`, expected an object.', connection.getDebugName(), connectionID) : invariant(false) : undefined;

      var nodeData = edgeData[NODE];
      if (nodeData == null) {
        return;
      }

      !(typeof nodeData === 'object') ? process.env.NODE_ENV !== 'production' ? invariant(false, 'RelayQueryWriter: Expected node to be an object for field `%s` on ' + 'record `%s`.', connection.getDebugName(), connectionID) : invariant(false) : undefined;

      // For consistency, edge IDs are calculated from the connection & node ID.
      // A node ID is only generated if the node does not have an id and
      // there is no existing edge.
      var prevEdge = filteredEdges[nextIndex++];
      var nodeID = nodeData && nodeData[ID] || prevEdge && _this3._store.getLinkedRecordID(prevEdge.edgeID, NODE) || generateClientID();
      // TODO: Flow: `nodeID` is `string`
      var edgeID = generateClientEdgeID(connectionID, nodeID);
      var path = state.path.getPath(edges, edgeID);
      _this3.createRecordIfMissing(edges, edgeID, null, path);
      fetchedEdgeIDs.push(edgeID);

      // Write data for the edge, using `nodeID` as the id for direct descendant
      // `node` fields. This is necessary for `node`s that do not have an `id`,
      // which would cause the generated ID here to not match the ID generated
      // in `_writeLink`.
      _this3.traverse(edges, {
        nodeID: nodeID,
        path: path,
        recordID: edgeID,
        responseData: edgeData
      });
      isUpdate = isUpdate || _this3.hasChangeToRecord(edgeID);
    });

    var pageInfo = connectionData[PAGE_INFO] || RelayConnectionInterface.getDefaultPageInfo();
    this._store.putRangeEdges(connectionID, rangeCalls, pageInfo, fetchedEdgeIDs);

    // Only broadcast an update to the range if an edge was added/changed.
    // Node-level changes will broadcast at the node ID.
    if (isUpdate) {
      this.recordUpdate(connectionID);
    }
  };

  /**
   * Writes a plural linked field such as `actors`. The response data is
   * expected to be an array of item objects. These fields are similar to
   * connections, but do not support range calls such as `first` or `after`.
   */

  RelayQueryWriter.prototype._writePluralLink = function _writePluralLink(field, state, recordID, fieldData) {
    var _this4 = this;

    var storageKey = field.getStorageKey();
    !Array.isArray(fieldData) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'RelayQueryWriter: Expected array data for field `%s` on record `%s`.', field.getDebugName(), recordID) : invariant(false) : undefined;

    var prevLinkedIDs = this._store.getLinkedRecordIDs(recordID, storageKey);
    var nextLinkedIDs = [];
    var isUpdate = !prevLinkedIDs;
    var nextIndex = 0;
    fieldData.forEach(function (nextRecord) {
      // validate response data
      if (nextRecord == null) {
        return;
      }
      !(typeof nextRecord === 'object' && nextRecord) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'RelayQueryWriter: Expected elements for plural field `%s` to be ' + 'objects.', storageKey) : invariant(false) : undefined;

      // Reuse existing generated IDs if the node does not have its own `id`.
      // TODO: Flow: `nextRecord` is asserted as typeof === 'object'
      var prevLinkedID = prevLinkedIDs && prevLinkedIDs[nextIndex];
      var nextLinkedID = nextRecord[ID] || prevLinkedID || generateClientID();
      nextLinkedIDs.push(nextLinkedID);

      var path = state.path.getPath(field, nextLinkedID);
      var typeName = _this4.getRecordTypeName(field, nextLinkedID, nextRecord);
      _this4.createRecordIfMissing(field, nextLinkedID, typeName, path);
      isUpdate = isUpdate || nextLinkedID !== prevLinkedID || _this4.isNewRecord(nextLinkedID);

      _this4.traverse(field, {
        nodeID: null, // never propagate `nodeID` past the first linked field
        path: path,
        recordID: nextLinkedID,
        responseData: nextRecord
      });
      isUpdate = isUpdate || _this4.hasChangeToRecord(nextLinkedID);
      nextIndex++;
    });

    this._store.putLinkedRecordIDs(recordID, storageKey, nextLinkedIDs);

    // Check if length has changed
    isUpdate = isUpdate || !prevLinkedIDs || prevLinkedIDs.length !== nextLinkedIDs.length;

    // Only broadcast a list-level change if a record was changed/added
    if (isUpdate) {
      this.recordUpdate(recordID);
    }
  };

  /**
   * Writes a link from one record to another, for example linking the `viewer`
   * record to the `actor` record in the query `viewer { actor }`. The `field`
   * variable is the field being linked (`actor` in the example).
   */

  RelayQueryWriter.prototype._writeLink = function _writeLink(field, state, recordID, fieldData) {
    var nodeID = state.nodeID;

    var storageKey = field.getStorageKey();
    !(typeof fieldData === 'object' && fieldData !== null) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'RelayQueryWriter: Expected data for non-scalar field `%s` on record ' + '`%s` to be an object.', field.getDebugName(), recordID) : invariant(false) : undefined;

    // Prefer the actual `id` if present, otherwise generate one (if an id
    // was already generated it is reused). `node`s within a connection are
    // a special case as the ID used here must match the one generated prior to
    // storing the parent `edge`.
    // TODO: Flow: `fieldData` is asserted as typeof === 'object'
    var prevLinkedID = this._store.getLinkedRecordID(recordID, storageKey);
    var nextLinkedID = field.getSchemaName() === NODE && nodeID || fieldData[ID] || prevLinkedID || generateClientID();

    var path = state.path.getPath(field, nextLinkedID);
    var typeName = this.getRecordTypeName(field, nextLinkedID, fieldData);
    this.createRecordIfMissing(field, nextLinkedID, typeName, path);
    // always update the store to ensure the value is present in the appropriate
    // data sink (record/queuedRecords), but only record an update if the value
    // changed.
    this._store.putLinkedRecordID(recordID, storageKey, nextLinkedID);
    if (prevLinkedID !== nextLinkedID || this.isNewRecord(nextLinkedID)) {
      this.recordUpdate(recordID);
    }

    this.traverse(field, {
      nodeID: null,
      path: path,
      recordID: nextLinkedID,
      responseData: fieldData
    });
  };

  return RelayQueryWriter;
})(RelayQueryVisitor);

module.exports = RelayQueryWriter;