react-relay
Version:
A framework for building data-driven React applications.
462 lines (386 loc) • 19.1 kB
JavaScript
/**
* Copyright 2013-2015, 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.
*
* @providesModule RelayQueryWriter
*
* @typechecks
*/
'use strict';
var _inherits = require('babel-runtime/helpers/inherits')['default'];
var _classCallCheck = require('babel-runtime/helpers/class-call-check')['default'];
var RelayQuery = require('./RelayQuery');
var RelayConnectionInterface = require('./RelayConnectionInterface');
var RelayQueryVisitor = require('./RelayQueryVisitor');
var RelayRecordStatus = require('./RelayRecordStatus');
var generateClientEdgeID = require('./generateClientEdgeID');
var generateClientID = require('./generateClientID');
var invariant = require('fbjs/lib/invariant');
var warning = require('fbjs/lib/warning');
var EDGES = RelayConnectionInterface.EDGES;
var NODE = RelayConnectionInterface.NODE;
var PAGE_INFO = RelayConnectionInterface.PAGE_INFO;
var ID = 'id';
/**
* @internal
*
* Helper for writing the result of one or more queries/operations into the
* store, updating tracked queries, and recording changed record IDs.
*/
var RelayQueryWriter = (function (_RelayQueryVisitor) {
_inherits(RelayQueryWriter, _RelayQueryVisitor);
function RelayQueryWriter(store, queryTracker, changeTracker, options) {
_classCallCheck(this, RelayQueryWriter);
_RelayQueryVisitor.call(this);
this._changeTracker = changeTracker;
this._forceIndex = options && options.forceIndex ? options.forceIndex : 0;
this._store = store;
this._queryTracker = queryTracker;
this._updateTrackedQueries = !!(options && options.updateTrackedQueries);
}
RelayQueryWriter.prototype.getRecordStore = function getRecordStore() {
return this._store;
};
/**
* Traverses a query and payload in parallel, writing the results into the
* store.
*/
RelayQueryWriter.prototype.writePayload = function writePayload(node, recordID, responseData, path) {
var _this = this;
var state = {
nodeID: null,
recordID: recordID,
responseData: responseData,
path: path
};
if (node instanceof RelayQuery.Field && !node.isScalar()) {
// for non-scalar fields, the recordID is the parent
node.getChildren().forEach(function (child) {
_this.visit(child, state);
});
return;
}
this.visit(node, state);
};
/**
* Records are "created" whenever an entry did not previously exist for the
* `recordID`, including cases when a `recordID` is created with a null value.
*/
RelayQueryWriter.prototype.recordCreate = function recordCreate(recordID) {
this._changeTracker.createID(recordID);
};
/**
* Records are "updated" if any field changes (including being set to null).
* Updates are not recorded for newly created records.
*/
RelayQueryWriter.prototype.recordUpdate = function recordUpdate(recordID) {
this._changeTracker.updateID(recordID);
};
/**
* Determine if the record was created or updated by this write operation.
*/
RelayQueryWriter.prototype.hasChangeToRecord = function hasChangeToRecord(recordID) {
return this._changeTracker.hasChange(recordID);
};
/**
* Determine if the record was created by this write operation.
*/
RelayQueryWriter.prototype.isNewRecord = function isNewRecord(recordID) {
return this._changeTracker.isNewRecord(recordID);
};
/**
* Helper to create a record and the corresponding notification.
*/
RelayQueryWriter.prototype.createRecordIfMissing = function createRecordIfMissing(node, recordID, path) {
var recordStatus = this._store.getRecordStatus(recordID);
if (recordStatus !== RelayRecordStatus.EXISTENT) {
this._store.putRecord(recordID, path);
this.recordCreate(recordID);
}
if (this.isNewRecord(recordID) || this._updateTrackedQueries) {
this._queryTracker.trackNodeForID(node, recordID, path);
}
};
RelayQueryWriter.prototype.visitRoot = function visitRoot(root, state) {
var path = state.path;
var recordID = state.recordID;
var responseData = state.responseData;
var recordStatus = this._store.getRecordStatus(recordID);
// GraphQL should never return undefined for a field
if (responseData == null) {
!(responseData !== undefined) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'RelayQueryWriter: Unexpectedly encountered `undefined` in payload. ' + 'Cannot set root record `%s` to undefined.', recordID) : invariant(false) : undefined;
this._store.deleteRecord(recordID);
if (recordStatus === RelayRecordStatus.EXISTENT) {
this.recordUpdate(recordID);
}
return;
}
if (recordStatus !== RelayRecordStatus.EXISTENT) {
this._store.putRecord(recordID, path);
this.recordCreate(recordID);
}
if (this.isNewRecord(recordID) || this._updateTrackedQueries) {
this._queryTracker.trackNodeForID(root, recordID, path);
}
this.traverse(root, state);
};
RelayQueryWriter.prototype.visitField = function visitField(field, state) {
var recordID = state.recordID;
var responseData = state.responseData;
!(this._store.getRecordStatus(recordID) === RelayRecordStatus.EXISTENT) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'RelayQueryWriter: Cannot update a non-existent record, `%s`.', recordID) : invariant(false) : undefined;
!(typeof responseData === 'object' && responseData !== null) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'RelayQueryWriter: Cannot update record `%s`, expected response to be ' + 'an object.', recordID) : invariant(false) : undefined;
// handle missing data
var fieldData = responseData[field.getSerializationKey()];
if (fieldData === undefined) {
return;
}
if (fieldData === null) {
this._store.deleteField(recordID, field.getStorageKey());
this.recordUpdate(recordID);
return;
}
if (field.isScalar()) {
this._writeScalar(field, state, recordID, fieldData);
} else if (field.isConnection()) {
this._writeConnection(field, state, recordID, fieldData);
} else if (field.isPlural()) {
this._writePluralLink(field, state, recordID, fieldData);
} else {
this._writeLink(field, state, recordID, fieldData);
}
};
/**
* Writes the value for a 'scalar' field such as `id` or `name`. The response
* data is expected to be scalar values or arrays of scalar values.
*/
RelayQueryWriter.prototype._writeScalar = function _writeScalar(field, state, recordID, nextValue) {
var storageKey = field.getStorageKey();
var prevValue = this._store.getField(recordID, storageKey);
// 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._store.putField(recordID, storageKey, nextValue);
// TODO: Flow: `nextValue` is an array, array indexing should work
if (Array.isArray(prevValue) && Array.isArray(nextValue) && prevValue.length === nextValue.length && prevValue.every(function (prev, ii) {
return prev === nextValue[ii];
})) {
return;
} else if (prevValue === nextValue) {
return;
}
this.recordUpdate(recordID);
};
/**
* Writes data for connection fields such as `news_feed` or `friends`. The
* response data is expected to be array of edge objects.
*/
RelayQueryWriter.prototype._writeConnection = function _writeConnection(field, state, recordID, connectionData) {
// 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).
var storageKey = field.getStorageKey();
var connectionID = this._store.getLinkedRecordID(recordID, storageKey);
if (!connectionID) {
connectionID = generateClientID();
}
var connectionRecordStatus = this._store.getRecordStatus(connectionID);
var hasEdges = !!(field.getFieldByStorageKey(EDGES) || connectionData != null && typeof connectionData === 'object' && connectionData[EDGES]);
var path = state.path.getPath(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._store.putRecord(connectionID, path);
this._store.putLinkedRecordID(recordID, storageKey, connectionID);
// record the create/update only if something changed
if (connectionRecordStatus !== RelayRecordStatus.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._store.hasRange(connectionID) || this._forceIndex && this._forceIndex > this._store.getRangeForceIndex(connectionID))) {
this._store.putRange(connectionID, field.getCallsWithValues(), this._forceIndex);
this.recordUpdate(connectionID);
}
var connectionState = {
path: path,
nodeID: null,
recordID: connectionID,
responseData: connectionData
};
this._traverseConnection(field, field, connectionState);
};
/**
* Recurse through connection subfields and write their results. This is
* necessary because handling an `edges` field also requires information about
* the parent connection field (see `_writeEdges`).
*/
RelayQueryWriter.prototype._traverseConnection = function _traverseConnection(connection, // the parent connection
node, // the parent connection or an intermediary fragment
state) {
var _this2 = this;
node.getChildren().forEach(function (child) {
if (child instanceof RelayQuery.Field) {
if (child.getSchemaName() === EDGES) {
_this2._writeEdges(connection, child, state);
} else if (child.getSchemaName() !== PAGE_INFO) {
// Page info is handled by the range
// Otherwise, write metadata fields normally (ex: `count`)
_this2.visit(child, state);
}
} else {
// Fragment case, recurse keeping track of parent connection
_this2._traverseConnection(connection, child, state);
}
});
};
/**
* Update a connection with newly fetched edges.
*/
RelayQueryWriter.prototype._writeEdges = function _writeEdges(connection, edges, state) {
var _this3 = this;
var connectionID = state.recordID;
var connectionData = state.responseData;
var storageKey = connection.getStorageKey();
!(typeof connectionData === 'object' && connectionData !== null) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'RelayQueryWriter: Cannot write edges for malformed connection `%s` on ' + 'record `%s`, expected the response to be an object.', storageKey, connectionID) : invariant(false) : undefined;
var edgesData = connectionData[EDGES];
// Validate response data.
if (edgesData == null) {
process.env.NODE_ENV !== 'production' ? warning(false, 'RelayQueryWriter: Cannot write edges for connection `%s` on record ' + '`%s`, expected a response for field `edges`.', storageKey, connectionID) : undefined;
return;
}
!Array.isArray(edgesData) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'RelayQueryWriter: Cannot write edges for connection `%s` on record ' + '`%s`, expected `edges` to be an array.', storageKey, connectionID) : invariant(false) : undefined;
var rangeCalls = connection.getCallsWithValues();
!RelayConnectionInterface.hasRangeCalls(rangeCalls) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'RelayQueryWriter: Cannot write edges for connection `%s` on record ' + '`%s` without `first`, `last`, or `find` argument.', storageKey, connectionID) : invariant(false) : undefined;
var rangeInfo = this._store.getRangeMetadata(connectionID, rangeCalls);
!rangeInfo ? process.env.NODE_ENV !== 'production' ? invariant(false, 'RelayQueryWriter: Expected a range to exist for connection field `%s` ' + 'on record `%s`.', storageKey, connectionID) : invariant(false) : undefined;
var fetchedEdgeIDs = [];
var isUpdate = false;
var nextIndex = 0;
var requestedEdges = rangeInfo.requestedEdges;
// Traverse connection edges, reusing existing edges if they exist
edgesData.forEach(function (edgeData) {
// validate response data
if (edgeData == null) {
return;
}
!(typeof edgeData === 'object' && edgeData) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'RelayQueryWriter: Cannot write edge for connection field `%s` on ' + 'record `%s`, expected an object.', storageKey, connectionID) : invariant(false) : undefined;
var nodeData = edgeData[NODE];
if (nodeData == null) {
return;
}
!(typeof nodeData === 'object') ? process.env.NODE_ENV !== 'production' ? invariant(false, 'RelayQueryWriter: Expected node to be an object for field `%s` on ' + 'record `%s`.', storageKey, connectionID) : invariant(false) : undefined;
// 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.
var prevEdge = requestedEdges[nextIndex++];
var nodeID = nodeData && nodeData[ID] || prevEdge && _this3._store.getLinkedRecordID(prevEdge.edgeID, NODE) || generateClientID();
// TODO: Flow: `nodeID` is `string`
var edgeID = generateClientEdgeID(connectionID, nodeID);
var path = state.path.getPath(edges, edgeID);
_this3.createRecordIfMissing(edges, edgeID, path);
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`.
_this3.traverse(edges, {
path: path,
nodeID: nodeID,
recordID: edgeID,
responseData: edgeData
});
isUpdate = isUpdate || _this3.hasChangeToRecord(edgeID);
});
var pageInfo = connectionData[PAGE_INFO] || RelayConnectionInterface.getDefaultPageInfo();
this._store.putRangeEdges(connectionID, rangeCalls, pageInfo, fetchedEdgeIDs);
// Only broadcast an update to the range if an edge was added/changed.
// Node-level changes will broadcast at the node ID.
if (isUpdate) {
this.recordUpdate(connectionID);
}
};
/**
* Writes a plural linked field such as `actors`. The response data is
* expected to be an array of item objects. These fields are similar to
* connections, but do not support range calls such as `first` or `after`.
*/
RelayQueryWriter.prototype._writePluralLink = function _writePluralLink(field, state, recordID, fieldData) {
var _this4 = this;
var storageKey = field.getStorageKey();
!Array.isArray(fieldData) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'RelayQueryWriter: Expected array data for field `%s` on record `%s`.', storageKey, recordID) : invariant(false) : undefined;
var prevLinkedIDs = this._store.getLinkedRecordIDs(recordID, storageKey);
var nextLinkedIDs = [];
var isUpdate = !prevLinkedIDs;
var nextIndex = 0;
fieldData.forEach(function (nextRecord) {
// validate response data
if (nextRecord == null) {
return;
}
!(typeof nextRecord === 'object' && nextRecord) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'RelayQueryWriter: Expected elements for plural field `%s` to be ' + 'objects.', storageKey) : invariant(false) : undefined;
// Reuse existing generated IDs if the node does not have its own `id`.
// TODO: Flow: `nextRecord` is asserted as typeof === 'object'
var prevLinkedID = prevLinkedIDs && prevLinkedIDs[nextIndex];
var nextLinkedID = nextRecord[ID] || prevLinkedID || generateClientID();
nextLinkedIDs.push(nextLinkedID);
var path = state.path.getPath(field, nextLinkedID);
_this4.createRecordIfMissing(field, nextLinkedID, path);
isUpdate = isUpdate || nextLinkedID !== prevLinkedID || _this4.isNewRecord(nextLinkedID);
_this4.traverse(field, {
path: path,
nodeID: null, // never propagate `nodeID` past the first linked field
recordID: nextLinkedID,
responseData: nextRecord
});
isUpdate = isUpdate || _this4.hasChangeToRecord(nextLinkedID);
nextIndex++;
});
this._store.putLinkedRecordIDs(recordID, storageKey, nextLinkedIDs);
// Only broadcast a list-level change if a record was changed/added
if (isUpdate) {
this.recordUpdate(recordID);
}
};
/**
* 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).
*/
RelayQueryWriter.prototype._writeLink = function _writeLink(field, state, recordID, fieldData) {
var nodeID = state.nodeID;
var storageKey = field.getStorageKey();
!(typeof fieldData === 'object') ? process.env.NODE_ENV !== 'production' ? invariant(false, 'RelayQueryWriter: Expected data for non-scalar field `%s` on record ' + '`%s` to be an object.', storageKey, recordID) : invariant(false) : undefined;
// 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`.
// TODO: Flow: `fieldData` is asserted as typeof === 'object'
var prevLinkedID = this._store.getLinkedRecordID(recordID, storageKey);
var nextLinkedID = field.getSchemaName() === NODE && nodeID || fieldData[ID] || prevLinkedID || generateClientID();
var path = state.path.getPath(field, nextLinkedID);
this.createRecordIfMissing(field, nextLinkedID, path);
// 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.
this._store.putLinkedRecordID(recordID, storageKey, nextLinkedID);
if (prevLinkedID !== nextLinkedID || this.isNewRecord(nextLinkedID)) {
this.recordUpdate(recordID);
}
this.traverse(field, {
path: path,
nodeID: null,
recordID: nextLinkedID,
responseData: fieldData
});
};
return RelayQueryWriter;
})(RelayQueryVisitor);
module.exports = RelayQueryWriter;