/** * Create an empty record at `dataID` if a record does not already exist. */ putRecord( dataID: DataID, typeName: ?string, path?: RelayQueryPath ): void { const prevRecord = this._getRecordForWrite(dataID); if (prevRecord) { return; } const nextRecord = RelayRecord.createWithFields(dataID, { __typename: typeName, }); if (this._isOptimisticWrite) { this._setClientMutationID(nextRecord); } if (RelayRecord.isClientID(dataID)) { invariant( path, 'RelayRecordWriter.putRecord(): Expected a path for non-refetchable ' + 'record `%s`.', dataID ); nextRecord[PATH] = path; } this._records[dataID] = nextRecord; const cacheWriter = this._cacheWriter; if (!this._isOptimisticWrite && cacheWriter) { cacheWriter.writeField(dataID, '__dataID__', dataID, typeName); } }
trackNodeForID( node: RelayQuery.Node, dataID: DataID, path: ?RelayQueryPath ): void { // Non-refetchable nodes are tracked via their nearest-refetchable parent // (except for root call nodes) if (RelayRecord.isClientID(dataID)) { invariant( path, 'RelayQueryTracker.trackNodeForID(): Expected `path` for client ID, ' + '`%s`.', dataID ); if (!path.isRootPath()) { return; } } // Don't track `__type__` fields if (node instanceof RelayQuery.Field && node.getSchemaName() === TYPE) { return; } this._trackedNodesByID[dataID] = this._trackedNodesByID[dataID] || { trackedNodes: [], isMerged: false, }; this._trackedNodesByID[dataID].trackedNodes.push(node); this._trackedNodesByID[dataID].isMerged = false; }
/** * Given a query fragment and a data ID, returns a root query that applies * the fragment to the object specified by the data ID. */ buildFragmentQueryForDataID( fragment: RelayQuery.Fragment, dataID: DataID ): RelayQuery.Root { if (RelayRecord.isClientID(dataID)) { const path = this._queuedStore.getPathToRecord( this._rangeData.getCanonicalClientID(dataID), ); invariant( path, 'RelayStoreData.buildFragmentQueryForDataID(): Cannot refetch ' + 'record `%s` without a path.', dataID ); return path.getQuery(fragment); } // Fragment fields cannot be spread directly into the root because they // may not exist on the `Node` type. return RelayQuery.Root.build( fragment.getDebugName() || 'UnknownQuery', RelayNodeInterface.NODE, dataID, [fragment], {identifyingArgName: RelayNodeInterface.ID}, NODE_TYPE ); }
/** * Create an empty record at `dataID` if a record does not already exist. */ putRecord( dataID: DataID, typeName: ?string, path?: ?QueryPath ): void { const prevRecord = this._getRecordForWrite(dataID); if (prevRecord) { return; } const nextRecord = RelayRecord.createWithFields(dataID, { __typename: typeName, }); if (this._isOptimisticWrite) { /* $FlowIssue(>=0.23.0) #10620219 - After fixing some unsoundness in * dictionary types, we've come to realize we need a safer object * supertype than Object. */ this._setClientMutationID(nextRecord); } if (RelayRecord.isClientID(dataID) && path) { nextRecord[PATH] = path; } this._records[dataID] = nextRecord; const cacheWriter = this._cacheWriter; if (!this._isOptimisticWrite && cacheWriter) { cacheWriter.writeField(dataID, '__dataID__', dataID, typeName); } }
function buildEdgeField( parentID: DataID, edgeName: string, edgeFields: Array<RelayQuery.Node> ): RelayQuery.Field { const fields = [ RelayQuery.Field.build({ fieldName: 'cursor', type: 'String', }), RelayQuery.Field.build({ fieldName: TYPENAME, type: 'String', }), ]; if (RelayConnectionInterface.EDGES_HAVE_SOURCE_FIELD && !RelayRecord.isClientID(parentID)) { fields.push( RelayQuery.Field.build({ children: [ RelayQuery.Field.build({ fieldName: ID, type: 'String', }), RelayQuery.Field.build({ fieldName: TYPENAME, type: 'String', }), ], fieldName: 'source', metadata: {canHaveSubselections: true}, type: ANY_TYPE, }) ); } fields.push(...edgeFields); const edgeField = flattenRelayQuery(RelayQuery.Field.build({ children: fields, fieldName: edgeName, metadata: {canHaveSubselections: true}, type: ANY_TYPE, })); invariant( edgeField instanceof RelayQuery.Field, 'RelayMutationQuery: Expected a field.' ); return edgeField; }
(error, value) => { if (this._state === 'COMPLETED') { return; } if (error) { this._handleFailed(); return; } if (value && RelayRecord.isClientID(dataID)) { value.__path__ = pendingItems[0].path; } // Mark records as created/updated as necessary. Note that if the // record is known to be deleted in the store then it will have been // been marked as created already. Further, it does not need to be // updated since no additional data can be read about a deleted node. const recordState = this._store.getRecordState(dataID); if (recordState === 'UNKNOWN' && value !== undefined) { // Register immediately in case anything tries to read and subscribe // to this record (which means incrementing reference counts). if (this._garbageCollector) { this._garbageCollector.register(dataID); } // Mark as created if the store did not have a value but disk cache // did (either a known value or known deletion). this._changeTracker.createID(dataID); } else if (recordState === 'EXISTENT' && value != null) { // Mark as updated only if a record exists in both the store and // disk cache. this._changeTracker.updateID(dataID); } this._cachedRecords[dataID] = value; var items = this._pendingNodes[dataID]; delete this._pendingNodes[dataID]; if (this._cachedRecords[dataID] === undefined) { // We are out of luck if disk doesn't have the node either. this._handleFailed(); } else { items.forEach(item => { if (this._state === 'COMPLETED') { return; } this.visitNode(dataID, item); }); } if (this._isDone()) { this._handleSuccess(); } }
/** * Helper to create a record and the corresponding notification. */ createRecordIfMissing( node: RelayQuery.Node, recordID: DataID, path: QueryPath, payload: ?Object ): void { const recordState = this._store.getRecordState(recordID); const typeName = payload && this.getRecordTypeName(node, recordID, payload); this._writer.putRecord(recordID, typeName, path); if (recordState !== EXISTENT) { this.recordCreate(recordID); } if ( (this.isNewRecord(recordID) || this._updateTrackedQueries) && (!RelayRecord.isClientID(recordID) || RelayQueryPath.isRootPath(path)) ) { this._queryTracker.trackNodeForID(node, recordID, path); } }
/** * Given a query fragment and a data ID, returns a root query that applies * the fragment to the object specified by the data ID. */ buildFragmentQueryForDataID( fragment: RelayQuery.Fragment, dataID: DataID ): RelayQuery.Root { if (RelayRecord.isClientID(dataID)) { const path = this._queuedStore.getPathToRecord( this._rangeData.getCanonicalClientID(dataID), ); invariant( path, 'RelayStoreData.buildFragmentQueryForDataID(): Cannot refetch ' + 'record `%s` without a path.', dataID ); return RelayQueryPath.getQuery( this._cachedStore, path, fragment ); } // Fragment fields cannot be spread directly into the root because they // may not exist on the `Node` type. return RelayQuery.Root.build( fragment.getDebugName() || 'UnknownQuery', NODE, dataID, [idField, typeField, fragment], { identifyingArgName: ID, identifyingArgType: ID_TYPE, isAbstract: true, isDeferred: false, isPlural: false, }, NODE_TYPE ); }
/** * Create an empty record at `dataID` if a record does not already exist. */ putRecord( dataID: DataID, typeName: ?string, path?: ?QueryPath ): void { const prevRecord = this._getRecordForWrite(dataID); if (prevRecord) { return; } const nextRecord = RelayRecord.createWithFields(dataID, { __typename: typeName, }); if (this._isOptimisticWrite) { this._setClientMutationID(nextRecord); } if (RelayRecord.isClientID(dataID) && path) { nextRecord[PATH] = path; } this._records[dataID] = nextRecord; const cacheWriter = this._cacheWriter; if (!this._isOptimisticWrite && cacheWriter) { cacheWriter.writeField(dataID, '__dataID__', dataID, typeName); } }
type: 'root', }; export type QueryPath = ClientPath | NodePath | RootPath; /** * @internal * * Represents the path (root plus fields) within a query that fetched a * particular node. Each step of the path may represent a root query (for * refetchable nodes) or the field path from the nearest refetchable node. */ const RelayQueryPath = { createForID(dataID: DataID, name: string, routeName: ?string): QueryPath { invariant( !RelayRecord.isClientID(dataID), 'RelayQueryPath.createForID: Expected dataID to be a server id, got ' + '`%s`.', dataID ); return { dataID, name, routeName: routeName || '$RelayQuery', type: 'node', }; }, create(root: RelayQuery.Root): QueryPath { if (root.getFieldName() === NODE) { const identifyingArg = root.getIdentifyingArg();
/** * 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, }; }
type: 'root', }; export type QueryPath = ClientPath | NodePath | RootPath; /** * @internal * * Represents the path (root plus fields) within a query that fetched a * particular node. Each step of the path may represent a root query (for * refetchable nodes) or the field path from the nearest refetchable node. */ const RelayQueryPath = { createForID(dataID: DataID, name: string): QueryPath { invariant( !RelayRecord.isClientID(dataID), 'RelayQueryPath.createForID: Expected dataID to be a server id, got ' + '`%s`.', dataID ); return { dataID, name, type: 'node', }; }, create(root: RelayQuery.Root): QueryPath { if (root.getFieldName() === NODE) { const identifyingArg = root.getIdentifyingArg(); if (identifyingArg && typeof identifyingArg.value === 'string') {
isClientOnlyID: function(dataID: DataID): boolean { return RelayRecord.isClientID(dataID) && !clientIDToServerIDMap[dataID]; },