Home Reference Source Repository

src/traversal/__tests__/writeRelayQueryPayload_connectionField-test.js

/**
 * Copyright (c) 2013-present, 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.
 *
 * @emails oncall+relay
 */

'use strict';

require('configureForRelayOSS');

jest
  .unmock('GraphQLRange')
  .unmock('GraphQLSegment');

const GraphQLRange = require('GraphQLRange');
const Relay = require('Relay');
const RelayConnectionInterface = require('RelayConnectionInterface');
const RelayMetaRoute = require('RelayMetaRoute');
const RelayQuery = require('RelayQuery');
const RelayTestUtils = require('RelayTestUtils');

describe('writeRelayQueryPayload()', () => {
  let RelayRecordStore;
  let RelayRecordWriter;

  const {getNode, writePayload} = RelayTestUtils;
  let END_CURSOR, HAS_NEXT_PAGE, HAS_PREV_PAGE, PAGE_INFO, START_CURSOR;

  beforeEach(() => {
    jest.resetModuleRegistry();

    RelayRecordStore = require('RelayRecordStore');
    RelayRecordWriter = require('RelayRecordWriter');

    ({
      END_CURSOR,
      HAS_NEXT_PAGE,
      HAS_PREV_PAGE,
      PAGE_INFO,
      START_CURSOR,
    } = RelayConnectionInterface);

    jasmine.addMatchers(RelayTestUtils.matchers);
  });

  it('creates empty first() connection records', () => {
    const records = {};
    const store = new RelayRecordStore({records});
    const writer = new RelayRecordWriter(records, {}, false);
    const query = getNode(Relay.QL`
      query {
        node(id:"123") {
          friends(first:"3") {
            edges {
              cursor
              node {
                id
              }
              source {
                id
              }
            }
            pageInfo {
              hasNextPage
              hasPreviousPage
            }
          }
        }
      }
    `);

    const payload = {
      node: {
        id: '123',
        friends: {
          edges: [],
          [PAGE_INFO]: {
            [HAS_NEXT_PAGE]: false,
            [HAS_PREV_PAGE]: false,
          },
        },
        __typename: 'User',
      },
    };

    const results = writePayload(store, writer, query, payload);
    expect(results).toEqual({
      created: {
        '123': true,
        'client:1': true, // `friends` connection
      },
      updated: {},
    });
    expect(store.getRangeMetadata('client:1', [
      {name: 'first', value: 3},
    ])).toEqual({
      diffCalls: [],
      filterCalls: [],
      pageInfo: {
        [END_CURSOR]: undefined,
        [HAS_NEXT_PAGE]: false,
        [HAS_PREV_PAGE]: false,
        [START_CURSOR]: undefined,
      },
      requestedEdgeIDs: [],
      filteredEdges: [],
    });
  });

  it('creates first() connection records', () => {
    const records = {};
    const store = new RelayRecordStore({records});
    const writer = new RelayRecordWriter(records, {}, false);
    const query = getNode(Relay.QL`
      query {
        node(id:"123") {
          friends(first:"3") {
            edges {
              cursor
              node {
                id
              }
              source {
                id
              }
            }
            pageInfo {
              hasNextPage
              hasPreviousPage
            }
          }
        }
      }
    `);
    const payload = {
      node: {
        id: '123',
        friends: {
          edges: [
            {
              cursor: 'friend1',
              node: {
                id: 'friend1ID',
              },
              source: {
                id: '123',
              },
            },
            {
              cursor: 'friend2',
              node: {
                id: 'friend2ID',
              },
              source: {
                id: '123',
              },
            },
            {
              cursor: 'friend3',
              node: {
                id: 'friend3ID',
              },
              source: {
                id: '123',
              },
            },
          ],
          [PAGE_INFO]: {
            [HAS_NEXT_PAGE]: true,
            [HAS_PREV_PAGE]: false,
          },
        },
        __typename: 'User',
      },
    };
    const results = writePayload(store, writer, query, payload);
    expect(results).toEqual({
      created: {
        '123': true,
        'client:1': true, // `friends` connection
        'client:client:1:friend1ID': true,  // edges
        'client:client:1:friend2ID': true,
        'client:client:1:friend3ID': true,
        'friend1ID': true, // nodes
        'friend2ID': true,
        'friend3ID': true,
      },
      updated: {},
    });
    expect(store.getField('friend1ID', 'id')).toBe('friend1ID');
    expect(store.getField('friend2ID', 'id')).toBe('friend2ID');
    expect(store.getField('friend3ID', 'id')).toBe('friend3ID');
    expect(store.getRangeMetadata('client:1', [
      {name: 'first', value: 3},
    ])).toEqual({
      diffCalls: [],
      filterCalls: [],
      pageInfo: {
        [END_CURSOR]: 'friend3',
        [HAS_NEXT_PAGE]: true,
        [HAS_PREV_PAGE]: false,
        [START_CURSOR]: 'friend1',
      },
      requestedEdgeIDs: [
        'client:client:1:friend1ID',
        'client:client:1:friend2ID',
        'client:client:1:friend3ID',
      ],
      filteredEdges: [
        {edgeID: 'client:client:1:friend1ID', nodeID: 'friend1ID'},
        {edgeID: 'client:client:1:friend2ID', nodeID: 'friend2ID'},
        {edgeID: 'client:client:1:friend3ID', nodeID: 'friend3ID'},
      ],
    });
  });


  it('creates first() connection records when connection node is cached', () => {
    const cachedRecords = {
      123: {
        friends: {__dataID__: 'client:customid'},
      },
      'client:customid': {
        __dataID__: 'client:customid',
        __range__: new GraphQLRange(),
      },
    };
    const records = {};
    const store = new RelayRecordStore({records, cachedRecords});
    const writer = new RelayRecordWriter(records, {}, false);
    const query = getNode(Relay.QL`
      query {
        node(id:"123") {
          friends(first:"3") {
            edges {
              cursor
              node {
                id
              }
              source {
                id
              }
            }
            pageInfo {
              hasNextPage
              hasPreviousPage
            }
          }
        }
      }
    `);
    const payload = {
      node: {
        id: '123',
        friends: {
          edges: [
            {
              cursor: 'friend1',
              node: {
                id: 'friend1ID',
              },
              source: {
                id: '123',
              },
            },
            {
              cursor: 'friend2',
              node: {
                id: 'friend2ID',
              },
              source: {
                id: '123',
              },
            },
            {
              cursor: 'friend3',
              node: {
                id: 'friend3ID',
              },
              source: {
                id: '123',
              },
            },
          ],
          [PAGE_INFO]: {
            [HAS_NEXT_PAGE]: true,
            [HAS_PREV_PAGE]: false,
          },
        },
        __typename: 'User',
      },
    };
    const results = writePayload(store, writer, query, payload);
    expect(results).toEqual({
      created: {
        'client:client:customid:friend1ID': true,  // edges
        'client:client:customid:friend2ID': true,
        'client:client:customid:friend3ID': true,
        'friend1ID': true, // nodes
        'friend2ID': true,
        'friend3ID': true,
      },
      updated: {
        '123': true,
        'client:customid': true,  // `friends` connection, reusing client id.

      },
    });
    expect(store.getField('friend1ID', 'id')).toBe('friend1ID');
    expect(store.getField('friend2ID', 'id')).toBe('friend2ID');
    expect(store.getField('friend3ID', 'id')).toBe('friend3ID');
    expect(store.getRangeMetadata('client:customid', [
      {name: 'first', value: 3},
    ])).toEqual({
      diffCalls: [],
      filterCalls: [],
      pageInfo: {
        [END_CURSOR]: 'friend3',
        [HAS_NEXT_PAGE]: true,
        [HAS_PREV_PAGE]: false,
        [START_CURSOR]: 'friend1',
      },
      requestedEdgeIDs: [
        'client:client:customid:friend1ID',
        'client:client:customid:friend2ID',
        'client:client:customid:friend3ID',
      ],
      filteredEdges: [
        {edgeID: 'client:client:customid:friend1ID', nodeID: 'friend1ID'},
        {edgeID: 'client:client:customid:friend2ID', nodeID: 'friend2ID'},
        {edgeID: 'client:client:customid:friend3ID', nodeID: 'friend3ID'},
      ],
    });
  });

  it('skips over null edges and nodes', () => {
    const records = {};
    const store = new RelayRecordStore({records});
    const writer = new RelayRecordWriter(records, {}, false);
    const query = getNode(Relay.QL`
      query {
        node(id:"123") {
          friends(first:"3") {
            edges {
              cursor
              node {
                id
              }
            }
            pageInfo {
              hasNextPage
              hasPreviousPage
            }
          }
        }
      }
    `);
    const payload = {
      node: {
        id: '123',
        friends: {
          edges: [
            null,
            {
              cursor: 'friend2',
              node: null,
            },
            {
              cursor: 'friend3',
              node: {
                id: 'friend3ID',
              },
            },
          ],
          [PAGE_INFO]: {
            [HAS_NEXT_PAGE]: true,
            [HAS_PREV_PAGE]: false,
          },
        },
        __typename: 'User',
      },
    };
    const results = writePayload(store, writer, query, payload);
    expect(results).toEqual({
      created: {
        '123': true,
        'client:1': true, // `friends` connection
        'client:client:1:friend3ID': true, // edges
        'friend3ID': true,
      },
      updated: {},
    });
    expect(store.getField('friend3ID', 'id')).toBe('friend3ID');
    expect(store.getRangeMetadata('client:1', [
      {name: 'first', value: 1},
    ])).toEqual({
      diffCalls: [],
      filterCalls: [],
      pageInfo: {
        [END_CURSOR]: 'friend3',
        [HAS_NEXT_PAGE]: true,
        [HAS_PREV_PAGE]: false,
        [START_CURSOR]: 'friend3',
      },
      requestedEdgeIDs: ['client:client:1:friend3ID'],
      filteredEdges: [
        {edgeID: 'client:client:1:friend3ID', nodeID: 'friend3ID'},
      ],
    });
  });

  it('creates range when a connection record already exists', () => {
    const records = {};
    const store = new RelayRecordStore({records});
    const writer = new RelayRecordWriter(records, {}, false);
    let query = getNode(Relay.QL`
      query {
        node(id:"123") {
          friends {count}
        }
      }
    `);
    let payload = {
      node: {
        id: '123',
        friends: {count: 5},
        __typename: 'User',
      },
    };
    writePayload(store, writer, query, payload);

    query = getNode(Relay.QL`
      query {
        node(id:"123") {
          friends(first:"1") {
            edges {
              cursor
              node {
                id
              }
              source {
                id
              }
            }
            pageInfo {
              hasNextPage
              hasPreviousPage
            }
          }
        }
      }
    `);
    payload = {
      node: {
        id: '123',
        __typename: 'User',
        friends: {
          edges: [
            {
              cursor: 'friend1',
              node: {
                id: 'friend1ID',
              },
              source: {
                id: '123',
              },
            },
          ],
          [PAGE_INFO]: {
            [HAS_NEXT_PAGE]: true,
            [HAS_PREV_PAGE]: false,
          },
        },
      },
    };
    const results = writePayload(store, writer, query, payload);
    expect(results).toEqual({
      created: {
        'client:client:1:friend1ID': true,  // edges
        'friend1ID': true, // nodes
      },
      updated: {
        'client:1': true,
      },
    });
    expect(store.getField('friend1ID', 'id')).toBe('friend1ID');
    expect(store.getRangeMetadata('client:1', [
      {name: 'first', value: 1},
    ])).toEqual({
      diffCalls: [],
      filterCalls: [],
      pageInfo: {
        [END_CURSOR]: 'friend1',
        [HAS_NEXT_PAGE]: true,
        [HAS_PREV_PAGE]: false,
        [START_CURSOR]: 'friend1',
      },
      requestedEdgeIDs: ['client:client:1:friend1ID'],
      filteredEdges: [
        {edgeID: 'client:client:1:friend1ID', nodeID: 'friend1ID'},
      ],
    });
  });

  it('should throw when connection is missing required calls', () => {
    const records = {};
    const store = new RelayRecordStore({records});
    const writer = new RelayRecordWriter(records, {}, false);
    const edgesFragment = Relay.QL`
      fragment on FriendsConnection {
        edges {
          cursor
          node {
            id
          }
          source {
            id
          }
        }
        pageInfo {
          hasNextPage
          hasPreviousPage
        }
      }
    `;
    const query = getNode(Relay.QL`
      query {
        node(id:"123") {
          friends(isViewerFriend:true) {
            ${edgesFragment}
          }
        }
      }
    `);
    const payload = {
      node: {
        id: '123',
        friends: {
          edges: [
            {
              cursor: 'friend1',
              node: {
                id: 'friend1ID',
              },
              source: {
                id: '123',
              },
            },
          ],
          [PAGE_INFO]: {
            [HAS_NEXT_PAGE]: true,
            [HAS_PREV_PAGE]: false,
          },
        },
        __typename: 'User',
      },
    };
    expect(() => writePayload(store, writer, query, payload)).toFailInvariant(
      'RelayQueryWriter: Cannot write edges for connection ' +
      'on record `client:1` without `first`, `last`, or `find` argument.'
    );
  });

  describe('first() connections with existing data', () => {
    let store, writer;

    beforeEach(() => {
      const query = getNode(Relay.QL`
        query {
          node(id:"123") {
            friends(first:"1") {
              edges {
                node {
                  id
                }
              }
            }
          }
        }
      `);
      const payload = {
        node: {
          id: '123',
          friends: {
            edges: [{
              node: {
                id: 'node1',
              },
              cursor: 'cursor1',
            }],
            [PAGE_INFO]: {
              [HAS_NEXT_PAGE]: true,
              [HAS_PREV_PAGE]: false,
            },
          },
          __typename: 'User',
        },
      };
      const records = {};
      store = new RelayRecordStore({records});
      writer = new RelayRecordWriter(records, {}, false);
      writePayload(store, writer, query, payload);
    });

    it('appends new edges', () => {
      const query = getNode(Relay.QL`
        query {
          node(id:"123") {
            friends(first:"1",after:"cursor1") {
              edges {
                node {
                  id
                }
              }
            }
          }
        }
      `);
      const payload = {
        node: {
          id: '123',
          friends: {
            edges: [{
              node: {
                id: 'node2',
              },
              cursor: 'cursor2',
            }],
            [PAGE_INFO]: {
              [HAS_NEXT_PAGE]: true,
              [HAS_PREV_PAGE]: true,
            },
          },
          __typename: 'User',
        },
      };
      const results = writePayload(store, writer, query, payload);
      expect(results).toEqual({
        created: {
          'node2': true,
          'client:client:1:node2': true, // 2nd edge
        },
        updated: {
          'client:1': true, // range updated
        },
      });
      expect(store.getRangeMetadata('client:1', [
        {name: 'first', value: 2},
      ])).toEqual({
        diffCalls: [],
        filterCalls: [],
        pageInfo: {
          [END_CURSOR]: 'cursor2',
          [HAS_NEXT_PAGE]: true,
          [HAS_PREV_PAGE]: false,
          [START_CURSOR]: 'cursor1',
        },
        requestedEdgeIDs: [
          'client:client:1:node1',
          'client:client:1:node2',
        ],
        filteredEdges: [
          {edgeID: 'client:client:1:node1', nodeID: 'node1'},
          {edgeID: 'client:client:1:node2', nodeID: 'node2'},
        ],
      });
    });

    it('updates existing edges when ids match', () => {
      const query = getNode(Relay.QL`
        query {
          node(id:"123") {
            friends(first: "1") {
              edges {
                node {
                  id
                  name
                }
              }
            }
          }
        }
      `);
      const payload = {
        node: {
          id: '123',
          friends: {
            edges: [{
              node: {
                id: 'node1',
                name: 'Tim', // added field
              },
              cursor: 'cursor1',
            }],
            [PAGE_INFO]: {
              [HAS_NEXT_PAGE]: true,
              [HAS_PREV_PAGE]: true,
            },
          },
          __typename: 'User',
        },
      };
      const results = writePayload(store, writer, query, payload);
      expect(results).toEqual({
        created: {},
        updated: {
          'node1': true,    // `name` added
          // range not updated, only the node changed
        },
      });
      expect(store.getField('node1', 'name')).toBe('Tim');
      expect(store.getRangeMetadata('client:1', [
        {name: 'first', value: 1},
      ])).toEqual({
        diffCalls: [],
        filterCalls: [],
        pageInfo: {
          [END_CURSOR]: 'cursor1',
          [HAS_NEXT_PAGE]: true,
          [HAS_PREV_PAGE]: false,
          [START_CURSOR]: 'cursor1',
        },
        requestedEdgeIDs: ['client:client:1:node1'],
        filteredEdges: [
          {edgeID: 'client:client:1:node1', nodeID: 'node1'},
        ],
      });
    });

    it('updates the range when edge data changes', () => {
      // NOTE: Hack to preserve `source{id}` in all environments for now.
      const query = RelayQuery.Root.create(Relay.QL`
        query {
          node(id:"123") {
            friends(find:"node1") {
              edges {
                node {
                  id
                }
                source {
                  id
                }
              }
            }
          }
        }
      `, RelayMetaRoute.get('$RelayTest'), {});
      const payload = {
        node: {
          id: '123',
          friends: {
            edges: [{
              node: {
                id: 'node1',
              },
              source: { // new edge field
                id: '456',
              },
              cursor: 'cursor1',
            }],
            [PAGE_INFO]: {
              [HAS_NEXT_PAGE]: true,
              [HAS_PREV_PAGE]: true,
            },
          },
          __typename: 'User',
        },
      };
      const results = writePayload(store, writer, query, payload);
      expect(results).toEqual({
        created: {
          '456': true, // `source` added
        },
        updated: {
          'client:1': true, // range updated because an edge had a change
          'client:client:1:node1': true, // `source` added to edge
        },
      });
      expect(store.getRangeMetadata('client:1', [
        {name: 'first', value: 1},
      ])).toEqual({
        diffCalls: [],
        filterCalls: [],
        pageInfo: {
          [END_CURSOR]: 'cursor1',
          [HAS_NEXT_PAGE]: true,
          [HAS_PREV_PAGE]: false,
          [START_CURSOR]: 'cursor1',
        },
        requestedEdgeIDs: ['client:client:1:node1'],
        filteredEdges: [
          {edgeID: 'client:client:1:node1', nodeID: 'node1'},
        ],
      });
      const sourceID = store.getLinkedRecordID('client:client:1:node1', 'source');
      expect(sourceID).toBe('456');
      expect(store.getField(sourceID, 'id')).toBe('456');
    });

    it('does not overwrite edges when ids conflict', () => {
      const query = getNode(Relay.QL`
        query {
          node(id:"123") {
            friends(first:"1") {
              edges {
                node {
                  id
                }
              }
            }
          }
        }
      `);
      const payload = {
        node: {
          id: '123',
          friends: {
            edges: [{
              node: {
                id: 'node1b',
              },
              cursor: 'cursor1b',
            }],
            [PAGE_INFO]: {
              [HAS_NEXT_PAGE]: true,
              [HAS_PREV_PAGE]: false,
            },
          },
          __typename: 'User',
        },
      };
      const results = writePayload(store, writer, query, payload);
      const edgeID = 'client:client:1:node1b';
      expect(results).toEqual({
        created: {
          'node1b': true,
          [edgeID]: true,   // edge added but never referenced
        },
        updated: {
          'client:1': true,     // range updated
        },
      });
      expect(store.getRecordState(edgeID)).toBe('EXISTENT');
      expect(store.getLinkedRecordID(edgeID, 'node')).toBe('node1b');
      expect(store.getField('node1b', 'id')).toBe('node1b');
      expect(store.getRangeMetadata('client:1', [
        {name: 'first', value: 1},
      ])).toEqual({
        diffCalls: [],
        filterCalls: [],
        pageInfo: {
          [END_CURSOR]: 'cursor1',
          [HAS_NEXT_PAGE]: true,
          [HAS_PREV_PAGE]: false,
          [START_CURSOR]: 'cursor1',
        },
        requestedEdgeIDs: ['client:client:1:node1'],
        filteredEdges: [
          {edgeID: 'client:client:1:node1', nodeID: 'node1'},
        ],
      });
    });

    it('overwrites ranges when force index is set', () => {
      const query = getNode(Relay.QL`
        query {
          node(id:"123") {
            friends(first:"1") {
              edges {
                node {
                  id
                }
              }
            }
          }
        }
      `);
      const payload = {
        node: {
          id: '123',
          friends: {
            edges: [{
              node: {
                id: 'node1b',
              },
              cursor: 'cursor1b',
            }],
            [PAGE_INFO]: {
              [HAS_NEXT_PAGE]: true,
              [HAS_PREV_PAGE]: false,
            },
          },
          __typename: 'User',
        },
      };
      const results =
        writePayload(store, writer, query, payload, null, {forceIndex: 1});
      expect(results).toEqual({
        created: {
          'node1b': true,
          'client:client:1:node1b': true,
        },
        updated: {
          'client:1': true,     // range updated
        },
      });
      expect(store.getField('node1b', 'id')).toBe('node1b');
      expect(store.getRangeMetadata('client:1', [
        {name: 'first', value: 1},
      ])).toEqual({
        diffCalls: [],
        filterCalls: [],
        pageInfo: {
          [END_CURSOR]: 'cursor1b',
          [HAS_NEXT_PAGE]: true,
          [HAS_PREV_PAGE]: false,
          [START_CURSOR]: 'cursor1b',
        },
        requestedEdgeIDs: ['client:client:1:node1b'],
        filteredEdges: [
          {edgeID: 'client:client:1:node1b', nodeID: 'node1b'},
        ],
      });
    });
  });
});