Example #1
0
 it('returns the path for non-refetchable records', () => {
   const records = {};
   const store = new RelayRecordStore({records});
   const writer = new RelayRecordWriter(records, {}, false);
   const query = getNode(Relay.QL`
     query {
       viewer {
         actor {
           address {
             city
           }
         }
       }
     }
   `);
   const actorID = '123';
   const addressID = 'client:1';
   const path = RelayQueryPath.getPath(
     RelayQueryPath.getPath(
       RelayQueryPath.create(query),
       query.getFieldByStorageKey('actor'),
       actorID
     ),
     query.getFieldByStorageKey('actor').getFieldByStorageKey('address'),
     addressID
   );
   writer.putRecord(addressID, 'Type', path);
   expect(store.getPathToRecord(addressID)).toMatchPath(path);
 });
Example #2
0
    fieldData.forEach(nextRecord => {
      // validate response data
      if (nextRecord == null) {
        return;
      }
      invariant(
        typeof nextRecord === 'object' && nextRecord,
        'RelayQueryWriter: Expected elements for plural field `%s` to be ' +
        'objects.',
        storageKey
      );

      // Reuse existing generated IDs if the node does not have its own `id`.
      const prevLinkedID = prevLinkedIDs && prevLinkedIDs[nextIndex];
      const nextLinkedID = (
        nextRecord[ID] ||
        prevLinkedID ||
        generateClientID()
      );
      // $FlowFixMe(>=0.33.0)
      nextLinkedIDs.push(nextLinkedID);

      // $FlowFixMe(>=0.33.0)
      const path = RelayQueryPath.getPath(state.path, field, nextLinkedID);
      // $FlowFixMe(>=0.33.0)
      this.createRecordIfMissing(field, nextLinkedID, path, nextRecord);
      // $FlowFixMe(>=0.33.0)
      nextRecords[nextLinkedID] = {record: nextRecord, path};
      isUpdate = isUpdate || nextLinkedID !== prevLinkedID;
      nextIndex++;
    });
Example #3
0
    fieldData.forEach(nextRecord => {
      // validate response data
      if (nextRecord == null) {
        return;
      }
      invariant(
        typeof nextRecord === 'object' && nextRecord,
        'RelayQueryWriter: Expected elements for plural field `%s` to be ' +
        'objects.',
        storageKey
      );

      // Reuse existing generated IDs if the node does not have its own `id`.
      const prevLinkedID = prevLinkedIDs && prevLinkedIDs[nextIndex];
      const nextLinkedID = (
        nextRecord[ID] ||
        prevLinkedID ||
        generateClientID()
      );
      nextLinkedIDs.push(nextLinkedID);

      const path = RelayQueryPath.getPath(state.path, field, nextLinkedID);
      this.createRecordIfMissing(field, nextLinkedID, path, nextRecord);
      isUpdate = isUpdate || nextLinkedID !== prevLinkedID;

      this.traverse(field, {
        nodeID: null, // never propagate `nodeID` past the first linked field
        path,
        recordID: nextLinkedID,
        responseData: nextRecord,
      });
      nextIndex++;
    });
 const pendingItems = friendField.getChildren().map(node => {
   return {
     node,
     path: RelayQueryPath.getPath(dummyPath, friendField, 'friends_id'),
     rangeCalls: calls,
   };
 });
Example #5
0
  /**
   * Diff a field-of-fields such as `profile_picture {...}`. Returns early if
   * the field has not been fetched, otherwise the result of traversal.
   */
  diffLink(
    field: RelayQuery.Field,
    path: RelayQueryPath,
    dataID: DataID,
  ): ?DiffOutput {
    var nextDataID =
      this._store.getLinkedRecordID(dataID, field.getStorageKey());
    if (nextDataID === undefined) {
      return {
        diffNode: field,
        trackedNode: null,
      };
    }
    if (nextDataID === null) {
      return {
        diffNode: null,
        trackedNode: field,
      };
    }

    return this.traverse(
      field,
      path.getPath(field, nextDataID),
      makeScope(nextDataID)
    );
  }
Example #6
0
 _visitConnection(field: RelayQuery.Field, state: FinderState): void {
   var calls = field.getCallsWithValues();
   var dataID = this._store.getLinkedRecordID(
     state.dataID,
     field.getStorageKey()
   );
   if (dataID === undefined) {
     this._handleMissingData(field, state);
     return;
   }
   if (dataID) {
     var nextState: FinderState = {
       dataID,
       missingData: false,
       path: RelayQueryPath.getPath(state.path, field, dataID),
       rangeCalls: calls,
       rangeInfo: null,
     };
     var metadata = this._store.getRangeMetadata(dataID, calls);
     if (metadata) {
       nextState.rangeInfo = metadata;
     }
     this.traverse(field, nextState);
     state.missingData = state.missingData || nextState.missingData;
   }
 }
Example #7
0
  /**
   * Writes data for connection fields such as `news_feed` or `friends`. The
   * response data is expected to be array of edge objects.
   */
  _writeConnection(
    field: RelayQuery.Field,
    state: WriterState,
    recordID: DataID,
    connectionData: mixed
  ): void {
    // 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).
    const storageKey = field.getStorageKey();
    const connectionID =
      this._store.getLinkedRecordID(recordID, storageKey) ||
      generateClientID();

    const connectionRecordState = this._store.getRecordState(connectionID);
    const hasEdges = !!(
      field.getFieldByStorageKey(EDGES) ||
      (
        connectionData != null &&
        typeof connectionData === 'object' &&
        (connectionData: $FixMe)[EDGES]
      )
    );
    const path = RelayQueryPath.getPath(state.path, 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._writer.putRecord(connectionID, null, path);
    this._writer.putLinkedRecordID(recordID, storageKey, connectionID);
    // record the create/update only if something changed
    if (connectionRecordState !== 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._writer.hasRange(connectionID) ||
         (this._forceIndex &&
          this._forceIndex > this._store.getRangeForceIndex(connectionID)))) {
      this._writer.putRange(
        connectionID,
        field.getCallsWithValues(),
        this._forceIndex
      );
      this.recordUpdate(connectionID);
    }

    const connectionState = {
      nodeID: null,
      path,
      recordID: connectionID,
      responseData: connectionData,
    };
    this._traverseConnection(field, field, connectionState);
  }
Example #8
0
 _visitPlural(field: RelayQuery.Field, state: FinderState): void {
   var dataIDs = this._store.getLinkedRecordIDs(
     state.dataID,
     field.getStorageKey()
   );
   if (dataIDs === undefined) {
     this._handleMissingData(field, state);
     return;
   }
   if (dataIDs) {
     for (var ii = 0; ii < dataIDs.length; ii++) {
       if (state.missingData) {
         break;
       }
       var nextState = {
         dataID: dataIDs[ii],
         missingData: false,
         path: RelayQueryPath.getPath(state.path, field, dataIDs[ii]),
         rangeCalls: undefined,
         rangeInfo: undefined,
       };
       this.traverse(field, nextState);
       state.missingData = nextState.missingData;
     }
   }
 }
 it('returns pendingNodeStates when linked node is not in the store', () => {
   const queryNode = getNode(Relay.QL`
     fragment on Node {
       id
       friends {count}
     }
   `);
   const records = {
     '1055790163': {
       id: '1055790163',
       friends: { __dataID__: 'friends_id'},
       __dataID__: '1055790163',
       __typename: 'User',
     },
   };
   const result = findLeaves(
     queryNode,
     '1055790163',
     dummyPath,
     records,
   );
   const friendsField =  queryNode.getFieldByStorageKey('friends');
   const countField = friendsField.getFieldByStorageKey('count');
   expect(result.pendingNodeStates).toMatchPendingNodeStates([{
     dataID: 'friends_id',
     node: countField,
     path: RelayQueryPath.getPath(dummyPath, friendsField, 'friends_id'),
     rangeCalls: [],
   }]);
   expect(result.missingData).toBe(false);
 });
Example #10
0
 _visitEdges(field: RelayQuery.Field, state: FinderState): void {
   var rangeInfo = state.rangeInfo;
   // Doesn't have  `__range__` loaded
   if (!rangeInfo) {
     this._handleMissingData(field, state);
     return;
   }
   if (rangeInfo.diffCalls.length) {
     state.missingData = true;
     return;
   }
   var edgeIDs = rangeInfo.requestedEdgeIDs;
   for (var ii = 0; ii < edgeIDs.length; ii++) {
     if (state.missingData) {
       break;
     }
     var nextState = {
       dataID: edgeIDs[ii],
       missingData: false,
       path: RelayQueryPath.getPath(state.path, field, edgeIDs[ii]),
       rangeCalls: undefined,
       rangeInfo: undefined,
     };
     this.traverse(field, nextState);
     state.missingData = state.missingData || nextState.missingData;
   }
 }
Example #11
0
  visitFragment(
    fragment: RelayQuery.Fragment,
    state: WriterState
  ): void {
    const {recordID} = state;
    if (fragment.isDeferred()) {
      const hash = fragment.getSourceCompositeHash() || fragment.getCompositeHash();

      this._writer.setHasDeferredFragmentData(
        recordID,
        hash
      );

      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))
    ) {
      if (!this._isOptimisticUpdate && fragment.isTrackingEnabled()) {
        this._writer.setHasFragmentData(
          recordID,
          fragment.getCompositeHash()
        );
      }
      const path = RelayQueryPath.getPath(state.path, fragment, recordID);
      this.traverse(fragment, {
        ...state,
        path,
      });
    }
  }
 const pendingItems = edgeFields.map(node => {
   return {
     node,
     path: RelayQueryPath.getPath(dummyPath, edgeFields, 'edge_id'),
     rangeCalls: undefined,
   };
 });
Example #13
0
 linkedIDs.some(itemID => {
   const itemState = this.traverse(
     field,
     RelayQueryPath.getPath(path, field, itemID),
     makeScope(itemID)
   );
   return itemState && (itemState.diffNode || itemState.trackedNode);
 });
Example #14
0
    edgesData.forEach(edgeData => {
      // validate response data
      if (edgeData == null) {
        return;
      }
      invariant(
        typeof edgeData === 'object' && edgeData,
        'RelayQueryWriter: Cannot write edge for connection field `%s` on ' +
        'record `%s`, expected an object.',
        connection.getDebugName(),
        connectionID
      );

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

      invariant(
        typeof nodeData === 'object',
        'RelayQueryWriter: Expected node to be an object for field `%s` on ' +
        'record `%s`.',
        connection.getDebugName(),
        connectionID
      );

      // 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.
      const prevEdge = filteredEdges[nextIndex++];
      const nodeID = (
        (nodeData && nodeData[ID]) ||
        (prevEdge && this._store.getLinkedRecordID(prevEdge.edgeID, NODE)) ||
        generateClientID()
      );
      // TODO: Flow: `nodeID` is `string`
      // $FlowFixMe(>=0.33.0)
      const edgeID = generateClientEdgeID(connectionID, nodeID);
      const path = RelayQueryPath.getPath(state.path, edges, edgeID);
      this.createRecordIfMissing(edges, edgeID, path, null);
      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`.
      this.traverse(edges, {
        // $FlowFixMe(>=0.33.0)
        nodeID,
        path,
        recordID: edgeID,
        responseData: edgeData,
      });
      isUpdate = isUpdate || !prevEdge || edgeID !== prevEdge.edgeID;
    });
Example #15
0
  /**
   * Diffs the field conditionally based on the `scope` from the nearest
   * ancestor field.
   */
  visitField(
    node: RelayQuery.Field,
    path: RelayQueryPath,
    {connectionField, dataID, edgeID, rangeInfo}: DiffScope
  ): ?DiffOutput {
    // 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.canHaveSubselections()) {
      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);
    }
  }
Example #16
0
  /**
   * 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).
   */
  _writeLink(
    field: RelayQuery.Field,
    state: WriterState,
    recordID: DataID,
    fieldData: mixed
  ): void {
    const {nodeID} = state;
    const storageKey = field.getStorageKey();
    invariant(
      typeof fieldData === 'object' && fieldData !== null,
      'RelayQueryWriter: Expected data for non-scalar field `%s` on record ' +
      '`%s` to be an object.',
      field.getDebugName(),
      recordID
    );

    // 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`.
    const prevLinkedID = this._store.getLinkedRecordID(recordID, storageKey);
    const nextLinkedID = (
      (field.getSchemaName() === NODE && nodeID) ||
      fieldData[ID] ||
      prevLinkedID ||
      generateClientID()
    );

    // $FlowFixMe(>=0.33.0)
    const path = RelayQueryPath.getPath(state.path, field, nextLinkedID);
    // $FlowFixMe(>=0.33.0)
    this.createRecordIfMissing(field, nextLinkedID, path, fieldData);
    // 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.
    // $FlowFixMe(>=0.33.0)
    this._writer.putLinkedRecordID(recordID, storageKey, nextLinkedID);
    if (prevLinkedID !== nextLinkedID) {
      this.recordUpdate(recordID);
    }

    this.traverse(field, {
      nodeID: null,
      path,
      // $FlowFixMe(>=0.33.0)
      recordID: nextLinkedID,
      responseData: fieldData,
    });
  }
Example #17
0
 it('returns a client path given no `dataID`', () => {
   const query = getNode(Relay.QL`
     query {
       viewer {
         actor {
           id
         }
       }
     }
   `);
   const actor = query.getFieldByStorageKey('actor');
   const path = RelayQueryPath.getPath(query, actor);  // No `dataID`
   expect(path).toEqual({
     node: actor,
     parent: query,
     type: 'client',
   });
 });
Example #18
0
 filteredEdges.forEach(edge => {
   const scope = {
     connectionField: field,
     dataID: connectionID,
     edgeID: edge.edgeID,
     rangeInfo,
   };
   const diffOutput = this.traverse(
     field,
     RelayQueryPath.getPath(path, 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;
   }
 });
  it('returns pendingNodeStates when plural node is not in the store', () => {
    const queryNode = getNode(Relay.QL`
      fragment on Node {
        id
        screennames {service}
      }
    `);
    const records = {
      '1055790163': {
        id: '1055790163',
        __dataID__: '1055790163',
        __typename: 'User',
        screennames: [
          {__dataID__: 'client:screenname1'},
          {__dataID__: 'client:screenname2'},
        ],
      },
    };

    const result = findLeaves(
      queryNode,
      '1055790163',
      dummyPath,
      records
    );

    const screennamesField = queryNode.getFieldByStorageKey('screennames');
    const serviceField = screennamesField.getFieldByStorageKey('service');
    const path = RelayQueryPath.getPath(
      dummyPath,
      screennamesField,
      'client:screenname'
    );
    const partialPendingState = {
      node: serviceField,
      path,
      rangeCalls: undefined,
    };
    expect(result.pendingNodeStates).toMatchPendingNodeStates([
      {dataID: 'client:screenname1', ...partialPendingState},
      {dataID: 'client:screenname2', ...partialPendingState},
    ]);
    expect(result.missingData).toBe(false);
  });
Example #20
0
  it('creates paths to non-refetchable fields', () => {
    const query = getNode(Relay.QL`
      query {
        node(id:"123") {
          id
        }
      }
    `);
    const address = getNode(Relay.QL`
      fragment on Actor {
        address {
          city
        }
      }
    `).getFieldByStorageKey('address');
    const city = getNode(Relay.QL`
      fragment on StreetAddress {
        city
      }
    `).getFieldByStorageKey('city');

    // address is not refetchable, has client ID
    writer.putRecord('123', 'User');
    const root = RelayQueryPath.create(query);
    const path = RelayQueryPath.getPath(root, address, 'client:1');
    const pathQuery = RelayQueryPath.getQuery(store, path, city);
    expect(pathQuery).toEqualQueryRoot(getVerbatimNode(Relay.QL`
      query {
        node(id:"123") {
          ... on User {
            id
            __typename
            address {
              city
            }
          }
        }
      }
    `));
    expect(RelayQueryPath.getName(path)).toBe(query.getName());
    expect(pathQuery.getName()).toBe(query.getName());
    expect(pathQuery.getRoute().name).toBe(query.getRoute().name);
    expect(pathQuery.isAbstract()).toBe(true);
  });
Example #21
0
  it('warns if the root record\'s type is unknown', () => {
    const query = getNode(Relay.QL`
      query {
        viewer {
          actor {
            id
          }
        }
      }
    `);
    const actor = query.getFieldByStorageKey('actor');
    const fragment = Relay.QL`
      fragment on Node {
        name
      }
    `;

    // actor has an ID and is refetchable, but the type of actor is unknown.
    const root = RelayQueryPath.create(query);
    const path = RelayQueryPath.getPath(root, actor, '123');
    const pathQuery = RelayQueryPath.getQuery(store, path, getNode(fragment));
    expect(pathQuery).toEqualQueryRoot(getVerbatimNode(Relay.QL`
      query {
        node(id:"123") {
          # not wrapped in a concrete fragment because the type is unknown.
          ... on Node {
            name
            id
            __typename
          }
          id
          __typename
        }
      }
    `));
    expect(pathQuery.getName()).toBe(query.getName());
    expect(pathQuery.getRoute().name).toBe(query.getRoute().name);
    expect([
      'RelayQueryPath: No typename found for %s record `%s`. Generating a ' +
      'possibly invalid query.',
      'unknown',
      '123',
    ]).toBeWarnedNTimes(1);
  });
Example #22
0
 _visitLinkedField(field: RelayQuery.Field, state: FinderState): void {
   var dataID =
     this._store.getLinkedRecordID(state.dataID, field.getStorageKey());
   if (dataID === undefined) {
     this._handleMissingData(field, state);
     return;
   }
   if (dataID) {
     var nextState = {
       dataID,
       missingData: false,
       path: RelayQueryPath.getPath(state.path, field, dataID),
       rangeCalls: undefined,
       rangeInfo: undefined,
     };
     this.traverse(field, nextState);
     state.missingData = state.missingData || nextState.missingData;
   }
 }
Example #23
0
 filteredEdges.forEach(edge => {
   // Flow loses type information in closures
   if (rangeInfo && connectionID) {
     var scope = {
       connectionField: field,
       dataID: connectionID,
       edgeID: edge.edgeID,
       rangeInfo,
     };
     var diffOutput = this.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;
     }
   }
 });
Example #24
0
 linkedIDs.forEach(itemID => {
   var itemState = this.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) {
       this.splitQuery(buildRoot(
         itemID,
         itemState.diffNode.getChildren(),
         path.getName(),
         field.getType()
       ));
     }
   }
 });
Example #25
0
  it('creates roots for refetchable fields', () => {
    const query = getNode(Relay.QL`
      query {
        viewer {
          actor {
            id
          }
        }
      }
    `);
    const actor = query.getFieldByStorageKey('actor');
    const fragment = Relay.QL`
      fragment on Node {
        name
      }
    `;

    // actor has an ID and is refetchable
    writer.putRecord('123', 'User');
    const root = RelayQueryPath.create(query);
    const path = RelayQueryPath.getPath(root, actor, '123');
    const pathQuery = RelayQueryPath.getQuery(store, path, getNode(fragment));
    expect(pathQuery).toEqualQueryRoot(getVerbatimNode(Relay.QL`
      query {
        node(id:"123") {
          ... on User {
            id
            __typename
            ... on Node {
              id
              __typename
              name
            }
          }
        }
      }
    `));
    expect(pathQuery.getName()).toBe(query.getName());
    expect(pathQuery.getRoute().name).toBe(query.getRoute().name);
  });
Example #26
0
 it('returns undefined for refetchable records', () => {
   const records = {};
   const store = new RelayRecordStore({records});
   const writer = new RelayRecordWriter(records, {}, false);
   const query = getNode(Relay.QL`
     query {
       viewer {
         actor {
           id
         }
       }
     }
   `);
   const actorID = '123';
   const path = RelayQueryPath.getPath(
     RelayQueryPath.create(query),
     query.getFieldByStorageKey('actor'),
     actorID
   );
   writer.putRecord(actorID, 'Type', path);
   expect(store.getPathToRecord(actorID)).toBe(undefined);
 });
Example #27
0
  /**
   * 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).
   */
  diffConnectionEdge(
    connectionField: RelayQuery.Field,
    edgeField: RelayQuery.Field,
    path: RelayQueryPath,
    edgeID: DataID,
    rangeInfo: RangeInfo
  ): DiffOutput {

    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;
    var nodeID = this._store.getLinkedRecordID(edgeID, NODE);

    if (diffNode) {
      if (!nodeID || RelayRecord.isClientID(nodeID)) {
        warning(
          connectionField.isConnectionWithoutNodeID(),
          'RelayDiffQueryBuilder: Field `node` on connection `%s` cannot be ' +
          'retrieved if it does not have an `id` field. If you expect fields ' +
          'to be retrieved on this field, add an `id` field in the schema. ' +
          'If you choose to ignore this warning, you can silence it by ' +
          'adding `@relay(isConnectionWithoutNodeID: true)` to the ' +
          'connection field.',
          connectionField.getStorageKey()
        );
      } else {
        var {
          edges: diffEdgesField,
          node: diffNodeField,
        } = splitNodeAndEdgesFields(diffNode);

        // split missing `node` fields into a `node(id)` root query
        if (diffNodeField) {
          hasSplitQueries = true;
          const nodeField = edgeField.getFieldByStorageKey('node');
          invariant(
            nodeField,
            'RelayDiffQueryBuilder: Expected connection `%s` to have a ' +
            '`node` field.',
            connectionField.getSchemaName()
          );
          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 {
            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()
            );
          }
        }
      }
    }

    // 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,
    };
  }
 const pendingStates = edgeFields.map(node => ({
   dataID: 'edge_id',
   node,
   path: RelayQueryPath.getPath(dummyPath, edgeFields, 'edge_id'),
   rangeCalls: undefined,
 }));
 const pendingStates = friendField.getChildren().map(node => ({
   dataID: 'friends_id',
   node,
   path: RelayQueryPath.getPath(dummyPath, friendField, 'friends_id'),
   rangeCalls: calls,
 }));
    it('calls `onSuccess` when connection is on disk', () => {
      const queries = {
        q0: getNode(Relay.QL`
          query {
            node(id:"1055790163") {
              friends(first:"5") {
                edges {
                  node {
                    name
                  }
                  cursor
                }
              }
            }
          }
        `),
      };

      const diskCacheData = {
        '1055790163': {
          __dataID__: '1055790163',
          id: '1055790163',
          __typename: 'User',
          friends: {__dataID__: 'client:friends_id'},
        },
        'client:friends_id': {
          __dataID__: 'client:friends_id',
          __range__: new GraphQLRange(),
        },
        'client:edge_id': {
          __dataID__: 'client:edge_id',
          cursor: '1234',
          node: {__dataID__: 'friend_id'},
        },
        'friend_id': {
          __dataID__: 'friend_id',
          id: 'friend_id',
          name: 'name',
        },
      };

      const rangeInfo = {
        requestedEdgeIDs: ['client:edge_id'],
        diffCalls: [],
        pageInfo: {},
      };
      diskCacheData['client:friends_id'].__range__.retrieveRangeInfoForQuery
        .mockReturnValue(rangeInfo);
      const {cacheManager, callbacks, changeTracker, store} =
        performQueriesRestore(queries, {diskCacheData});

      expect(cacheManager.readRootCall.mock.calls.length).toBe(0);
      expect(cacheManager.readNode.mock.calls.length).toBe(1);
      expect(cacheManager.readNode.mock.calls[0][0]).toBe('1055790163');

      jest.runOnlyPendingTimers();
      expect(cacheManager.readRootCall.mock.calls.length).toBe(0);
      expect(cacheManager.readNode.mock.calls.length).toBe(2);
      expect(cacheManager.readNode.mock.calls[1][0]).toBe('client:friends_id');

      jest.runOnlyPendingTimers();
      expect(cacheManager.readRootCall.mock.calls.length).toBe(0);
      expect(cacheManager.readNode.mock.calls.length).toBe(3);
      expect(cacheManager.readNode.mock.calls[2][0]).toBe('client:edge_id');

      jest.runOnlyPendingTimers();
      expect(cacheManager.readRootCall.mock.calls.length).toBe(0);
      expect(cacheManager.readNode.mock.calls.length).toBe(4);
      expect(cacheManager.readNode.mock.calls[3][0]).toBe('friend_id');

      jest.runAllTimers();
      expect(cacheManager.readRootCall.mock.calls.length).toBe(0);
      expect(cacheManager.readNode.mock.calls.length).toBe(4);
      expect(callbacks.onFailure.mock.calls.length).toBe(0);
      expect(callbacks.onSuccess.mock.calls.length).toBe(1);
      expect(store.getRecordState('1055790163')).toBe('EXISTENT');
      expect(store.getField('1055790163', 'id')).toBe('1055790163');
      expect(store.getType('1055790163')).toBe('User');
      expect(store.getLinkedRecordID('1055790163', 'friends'))
        .toBe('client:friends_id');
      expect(store.getRecordState('client:friends_id')).toBe('EXISTENT');
      const query = queries.q0;
      const friendsField = query.getFieldByStorageKey('friends');
      const friendsPath = RelayQueryPath.getPath(
        RelayQueryPath.create(query),
        query.getFieldByStorageKey('friends'),
        'client:friends_id'
      );
      expect(store.getPathToRecord('client:friends_id'))
        .toMatchPath(friendsPath);
      expect(store.getRangeMetadata(
        'client:friends_id',
        [{name:'first', value: '5'}]
      )).toEqual({
        ...rangeInfo,
        filterCalls: [],
        filteredEdges: [{
          edgeID: 'client:edge_id',
          nodeID: 'friend_id',
        }],
      });
      expect(store.getRecordState('client:edge_id')).toBe('EXISTENT');
      const edgePath = RelayQueryPath.getPath(
        friendsPath,
        friendsField.getFieldByStorageKey('edges'),
        'client:edge_id'
      );
      expect(store.getPathToRecord('client:edge_id')).toMatchPath(edgePath);
      expect(store.getField('client:edge_id', 'cursor')).toBe('1234');
      expect(store.getLinkedRecordID('client:edge_id', 'node'))
        .toBe('friend_id');
      expect(store.getRecordState('friend_id')).toBe('EXISTENT');
      expect(store.getField('friend_id', 'id')).toBe('friend_id');
      expect(store.getField('friend_id', 'name')).toBe('name');
      expect(changeTracker.getChangeSet()).toEqual({
        created: {
          '1055790163': true,
          'client:friends_id': true,
          'client:edge_id': true,
          'friend_id': true,
        },
        updated: {},
      });
    });