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); });
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++; });
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, }; });
/** * 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) ); }
_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; } }
/** * 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); }
_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); });
_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; } }
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, }; });
linkedIDs.some(itemID => { const itemState = this.traverse( field, RelayQueryPath.getPath(path, field, itemID), makeScope(itemID) ); return itemState && (itemState.diffNode || itemState.trackedNode); });
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; });
/** * 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); } }
/** * 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, }); }
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', }); });
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); });
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); });
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); });
_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; } }
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; } } });
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() )); } } });
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); });
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); });
/** * 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: {}, }); });