src/traversal/__tests__/writeRelayQueryPayload_paths-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 Relay = require('Relay');
const RelayQueryPath = require('RelayQueryPath');
const RelayQueryTracker = require('RelayQueryTracker');
const RelayTestUtils = require('RelayTestUtils');
const invariant = require('invariant');
describe('writePayload()', () => {
let RelayRecordStore;
let RelayRecordWriter;
const {
getNode,
getVerbatimNode,
writeVerbatimPayload,
writePayload,
} = RelayTestUtils;
function getField(node, ...fieldNames) {
for (let ii = 0; ii < fieldNames.length; ii++) {
node = node.getFieldByStorageKey(fieldNames[ii]);
invariant(
node,
'getField(): Expected node to have field named `%s`.',
fieldNames[ii]
);
}
return node;
}
beforeEach(() => {
jest.resetModuleRegistry();
RelayRecordStore = require('RelayRecordStore');
RelayRecordWriter = require('RelayRecordWriter');
jasmine.addMatchers(RelayTestUtils.matchers);
});
describe('paths', () => {
it('writes path for id-less root records', () => {
const records = {};
const store = new RelayRecordStore({records});
const writer = new RelayRecordWriter(records, {}, false);
const query = getNode(Relay.QL`
query {
viewer {
actor {
id
}
}
}
`);
const payload = {
viewer: {
actor: {
id: '123',
__typename: 'User',
},
},
};
const results = writePayload(store, writer, query, payload);
expect(results).toEqual({
created: {
'client:1': true,
'123': true,
},
updated: {},
});
// viewer has a client id and must be refetched by the original root call
const path = RelayQueryPath.create(query);
expect(store.getRecordState('client:1')).toBe('EXISTENT');
expect(store.getPathToRecord('client:1')).toMatchPath(path);
// actor is refetchable by ID
expect(store.getPathToRecord('123')).toBe(undefined);
});
it('does not write paths to refetchable root records', () => {
const records = {};
const store = new RelayRecordStore({records});
const writer = new RelayRecordWriter(records, {}, false);
const query = getNode(Relay.QL`
query {
node(id:"123") {
id
}
}
`);
const payload = {
node: {
id: '123',
__typename: 'User',
},
};
const results = writePayload(store, writer, query, payload);
expect(results).toEqual({
created: {
'123': true,
},
updated: {},
});
expect(store.getRecordState('123')).toBe('EXISTENT');
expect(store.getPathToRecord('123')).toBe(undefined);
});
it('writes paths to non-refetchable linked 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 payload = {
viewer: {
actor: {
id: '123',
address: {
city: 'San Francisco',
},
__typename: 'User',
},
},
};
writePayload(store, writer, query, payload);
// linked nodes use a minimal path from the nearest refetchable node
const addressID = store.getLinkedRecordID('123', 'address');
const pathQuery = getNode(Relay.QL`
query {
node(id:"123") {
address {
city
}
}
}
`);
const path = RelayQueryPath.getPath(
RelayQueryPath.create(pathQuery),
getField(pathQuery, 'address'),
addressID
);
expect(store.getPathToRecord(addressID)).toMatchPath(path);
});
it('writes paths to plural linked fields', () => {
const records = {};
const store = new RelayRecordStore({records});
const writer = new RelayRecordWriter(records, {}, false);
const phone = {
isVerified: true,
phoneNumber: {
displayNumber: '1-800-555-1212', // directory assistance
countryCode: '1',
},
};
const query = getNode(Relay.QL`
query {
node(id:"123") {
allPhones {
isVerified
phoneNumber {
displayNumber
countryCode
}
}
}
}
`);
const payload = {
node: {
__typename: 'User',
id: '123',
allPhones: [phone],
},
};
writePayload(store, writer, query, payload);
// plural fields must be refetched through the parent
// get linked records to verify the client id
const allPhoneIDs = store.getLinkedRecordIDs('123', 'allPhones');
expect(allPhoneIDs.length).toBe(1);
let path = RelayQueryPath.getPath(
RelayQueryPath.create(query),
getField(query, 'allPhones'),
allPhoneIDs[0]
);
expect(store.getPathToRecord(allPhoneIDs[0])).toMatchPath(path);
// plural items must be refetched through the parent plural field
// get field to verify the client id is correct
const phoneNoID = store.getLinkedRecordID(allPhoneIDs[0], 'phoneNumber');
path = RelayQueryPath.getPath(
RelayQueryPath.getPath(
RelayQueryPath.create(query),
getField(query, 'allPhones'),
allPhoneIDs[0]
),
getField(query, 'allPhones', 'phoneNumber'),
phoneNoID
);
expect(store.getPathToRecord(phoneNoID)).toMatchPath(path);
});
it('writes paths to 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:"1") {
edges {
node {
id
address {
city
}
}
}
}
}
}
`);
const payload = {
node: {
__typename: 'User',
id: '123',
friends: {
edges: [
{
cursor: 'cursor1',
node: {
id: 'node1',
address: {
city: 'San Francisco',
},
},
},
],
},
},
};
writePayload(store, writer, query, payload);
// connections and edges must be refetched through the parent
let path = RelayQueryPath.getPath(
RelayQueryPath.create(query),
getField(query, 'friends'),
'client:1'
);
expect(store.getPathToRecord('client:1')).toMatchPath(path);
path = RelayQueryPath.getPath(
RelayQueryPath.getPath(
RelayQueryPath.create(query),
getField(query, 'friends'),
'client:1'
),
getField(query, 'friends', 'edges'),
'client:client:1:node1'
);
expect(store.getPathToRecord('client:client:1:node1')).toMatchPath(path);
// connection nodes with an ID are refetchable
expect(store.getPathToRecord('node1')).toBe(undefined);
// linked nodes use a minimal path from the nearest refetchable node
const pathQuery = getNode(Relay.QL`query{node(id:"node1"){address{city}}}`);
path = RelayQueryPath.getPath(
RelayQueryPath.create(pathQuery),
getField(pathQuery, 'address'),
'client:2'
);
expect(store.getField('client:2', 'city')).toBe('San Francisco');
expect(store.getPathToRecord('client:2')).toMatchPath(path);
});
it('writes paths with fragments', () => {
const records = {};
const rootCallMap = {};
const store = new RelayRecordStore({records}, {rootCallMap});
const writer = new RelayRecordWriter(records, rootCallMap, false);
const fragment = Relay.QL`fragment on Viewer {
actor {
id
__typename
name
}
}`;
const query = getVerbatimNode(Relay.QL`
query {
viewer {
${fragment}
}
}
`);
const payload = {
viewer: {
actor: {
name: 'Joe',
__typename: 'User',
},
},
};
writePayload(store, writer, query, payload);
const viewerID = store.getDataID('viewer');
const actorID = store.getLinkedRecordID(viewerID, 'actor');
const path = RelayQueryPath.getPath(
RelayQueryPath.getPath(
RelayQueryPath.create(query),
getNode(fragment),
viewerID
),
getNode(fragment).getChildren()[0],
actorID
);
expect(store.getPathToRecord(actorID)).toMatchPath(path);
});
});
describe('query tracking', () => {
it('tracks new root records', () => {
const records = {};
const store = new RelayRecordStore({records});
const writer = new RelayRecordWriter(records, {}, false);
const tracker = new RelayQueryTracker();
const query = getNode(Relay.QL`
query {
node(id:"123") {
id
name
}
}
`);
const payload = {
node: {
id: '123',
name: 'Joe',
__typename: 'User',
},
};
writePayload(store, writer, query, payload, tracker);
const trackedQueries = tracker.trackNodeForID.mock.calls;
expect(trackedQueries.length).toBe(1);
expect(trackedQueries[0][1]).toBe('123');
expect(trackedQueries[0][0]).toEqualQueryRoot(query);
});
it('tracks new records in fragments', () => {
const records = {};
const store = new RelayRecordStore({records});
const writer = new RelayRecordWriter(records, {}, false);
const tracker = new RelayQueryTracker();
// `address` will be encountered twice, both occurrences must be tracked
const fragment = Relay.QL`fragment on Node{address{city}}`;
const query = getNode(Relay.QL`
query {
node(id:"123") {
${fragment}
${fragment}
}
}
`);
const payload = {
node: {
id: '123',
address: {
city: 'San Francisco',
},
__typename: 'User',
},
};
writePayload(store, writer, query, payload, tracker);
const trackedQueries = tracker.trackNodeForID.mock.calls;
expect(trackedQueries.length).toBe(1);
expect(trackedQueries[0][1]).toBe('123');
expect(trackedQueries[0][0]).toEqualQueryNode(query);
});
it('tracks new linked records', () => {
const records = {
'client:1': {
__dataID__: 'client:1',
},
};
const store = new RelayRecordStore({records});
const writer = new RelayRecordWriter(records, {}, false);
const query = getNode(Relay.QL`
query {
viewer {
actor {
name
}
}
}
`);
const payload = {
viewer: {
actor: {
id: '123',
name: 'Joe',
__typename: 'User',
},
},
};
const tracker = new RelayQueryTracker();
writePayload(store, writer, query, payload, tracker);
const trackedQueries = tracker.trackNodeForID.mock.calls;
expect(trackedQueries.length).toBe(1);
expect(trackedQueries[0][1]).toBe('123');
expect(trackedQueries[0][0]).toEqualQueryNode(query.getChildren()[0]);
});
it('tracks new plural linked records', () => {
const records = {
'123': {
__dataID__: '123',
id: '123',
},
};
const store = new RelayRecordStore({records});
const writer = new RelayRecordWriter(records, {}, false);
const query = getNode(Relay.QL`
query {
node(id:"123") {
actors {
name
}
}
}
`);
const payload = {
node: {
__typename: 'User',
id: '123',
actors: [
{
id: '456',
name: 'Alice',
},
{
id: '789',
name: 'Bob',
},
],
},
};
const tracker = new RelayQueryTracker();
writePayload(store, writer, query, payload, tracker);
const trackedQueries = tracker.trackNodeForID.mock.calls;
expect(trackedQueries.length).toBe(2);
expect(trackedQueries[0][1]).toBe('456');
expect(trackedQueries[0][0]).toEqualQueryNode(
getField(query, 'actors')
);
expect(trackedQueries[1][1]).toBe('789');
expect(trackedQueries[1][0]).toEqualQueryNode(
getField(query, 'actors')
);
});
it('tracks new connections', () => {
const records = {
'123': {
__dataID__: '123',
id: '123',
},
};
const store = new RelayRecordStore({records});
const writer = new RelayRecordWriter(records, {}, false);
const query = getNode(Relay.QL`
query {
node(id:"123") {
friends(first:"1") {
edges {
node {
name
}
}
}
}
}
`);
const payload = {
node: {
__typename: 'User',
id: '123',
friends: {
edges: [
{
cursor: 'c1',
node: {
id: '456',
name: 'Greg',
},
},
],
},
},
};
const tracker = new RelayQueryTracker();
writePayload(store, writer, query, payload, tracker);
const trackedQueries = tracker.trackNodeForID.mock.calls;
expect(trackedQueries.length).toBe(1);
// track edges.node
expect(trackedQueries[0][1]).toBe('456');
expect(trackedQueries[0][0]).toEqualQueryNode(
getField(query, 'friends', 'edges', 'node')
);
});
it('tracks edges and nodes added to an existing connection', () => {
// write a range with first(1) edge
const records = {
'123': {
__dataID__: '123',
id: '123',
},
};
const store = new RelayRecordStore({records});
const writer = new RelayRecordWriter(records, {}, false);
let query = getNode(Relay.QL`
query {
node(id:"123") {
friends(first:"1") {
edges {
node {
name
}
}
}
}
}
`);
let payload = {
node: {
__typename: 'User',
id: '123',
friends: {
edges: [
{
cursor: 'c1',
node: {
id: '456',
name: 'Greg',
},
},
],
},
},
};
let tracker = new RelayQueryTracker();
writePayload(store, writer, query, payload, tracker);
expect(tracker.trackNodeForID.mock.calls.length).toBe(1);
// write an additional node and verify only the new edge and node are
// tracked
query = getNode(Relay.QL`
query {
node(id:"123") {
friends(after:"c1",first:"1") {
edges {
node {
name
}
}
}
}
}
`);
payload = {
node: {
id: '123',
friends: {
edges: [
{
cursor: 'c2',
node: {
id: '789',
name: 'Jing',
},
},
],
},
},
};
tracker = new RelayQueryTracker();
tracker.trackNodeForID.mockClear();
writePayload(store, writer, query, payload, tracker);
const trackedQueries = tracker.trackNodeForID.mock.calls;
expect(trackedQueries.length).toBe(1);
// track new node
expect(trackedQueries[0][1]).toBe('789');
expect(trackedQueries[0][0]).toEqualQueryNode(
getField(query, 'friends', 'edges', 'node')
);
});
it('re-tracks all nodes if `updateTrackedQueries` is enabled', () => {
const records = {};
const store = new RelayRecordStore({records});
const writer = new RelayRecordWriter(records, {}, false);
const query = getNode(Relay.QL`
query {
node(id:"123") {
name
allPhones {
phoneNumber {
displayNumber
}
}
friends(first:"1") {
edges {
node {
name
}
}
}
}
}
`);
const payload = {
node: {
id: '123',
name: 'Joe',
allPhones: [
{
phoneNumber: {
displayNumber: '1-800-555-1212', // directory assistance
},
},
],
friends: {
edges: [
{
cursor: 'c1',
node: {
id: '456',
name: 'Tim',
},
},
],
},
__typename: 'User',
},
};
// populate the store and record the original tracked queries
let queryTracker = new RelayQueryTracker();
writePayload(
store,
writer,
query,
payload,
queryTracker
);
const prevTracked = queryTracker.trackNodeForID.mock.calls.slice();
expect(prevTracked.length).toBe(2);
expect(prevTracked[0][1]).toBe('123'); // root
expect(prevTracked[1][1]).toBe('456'); // friends.edges.node
// rewriting the same payload by default does not track anything
queryTracker = new RelayQueryTracker();
queryTracker.trackNodeForID.mockClear();
writePayload(
store,
writer,
query,
payload,
queryTracker
);
expect(queryTracker.trackNodeForID.mock.calls.length).toBe(0);
// force-tracking should track the original nodes again
queryTracker = new RelayQueryTracker();
queryTracker.trackNodeForID.mockClear();
writePayload(
store,
writer,
query,
payload,
queryTracker,
{updateTrackedQueries: true}
);
const nextTracked = queryTracker.trackNodeForID.mock.calls;
expect(nextTracked.length).toBe(prevTracked.length);
nextTracked.forEach((tracked, ii) => {
expect(tracked[1]).toBe(prevTracked[ii][1]); // dataID
expect(tracked[0]).toEqualQueryNode(prevTracked[ii][0]); // dataID
});
});
});
it('skips non-matching fragments', () => {
const records = {};
const store = new RelayRecordStore({records});
const writer = new RelayRecordWriter(records, {}, false);
const query = getNode(Relay.QL`
query {
node(id: "123") {
...on User {
name
}
...on Comment {
body {
text
}
}
...on Node {
firstName
}
}
}
`);
const payload = {
node: {
id: '123',
__typename: 'User',
firstName: 'Joe',
name: 'Joe',
body: {
text: 'Skipped!',
},
},
};
writeVerbatimPayload(store, writer, query, payload);
expect(store.getField('123', 'firstName')).toBe('Joe');
expect(store.getField('123', 'name')).toBe('Joe');
// `body` only exists on `Comment` which does not match the record type
expect(store.getLinkedRecordID('123', 'body')).toBe(undefined);
});
});