UNPKG

react-relay

Version:

A framework for building data-driven React applications.

323 lines (290 loc) • 13.1 kB
/** * 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 RelayMutationQuery * @typechecks * */ 'use strict'; var _toConsumableArray = require('babel-runtime/helpers/to-consumable-array')['default']; var GraphQLStoreDataHandler = require('./GraphQLStoreDataHandler'); var RelayConnectionInterface = require('./RelayConnectionInterface'); var RelayMetaRoute = require('./RelayMetaRoute'); var RelayMutationType = require('./RelayMutationType'); var RelayQuery = require('./RelayQuery'); var RelayStoreData = require('./RelayStoreData'); var flattenRelayQuery = require('./flattenRelayQuery'); var forEachObject = require('fbjs/lib/forEachObject'); var nullthrows = require('fbjs/lib/nullthrows'); var inferRelayFieldsFromData = require('./inferRelayFieldsFromData'); var intersectRelayQuery = require('./intersectRelayQuery'); var invariant = require('fbjs/lib/invariant'); var refragmentRelayQuery = require('./refragmentRelayQuery'); var CLIENT_MUTATION_ID = RelayConnectionInterface.CLIENT_MUTATION_ID; /** * @internal * * Constructs query fragments that are sent with mutations, which should ensure * that any records changed as a result of mutations are brought up-to-date. * * The fragments are a minimal subset created by intersecting the "fat query" * (fields that a mutation declares may have changed) with the "tracked query" * (fields representing data previously queried and written into the store). */ var RelayMutationQuery = { /** * Accepts a mapping from field names to data IDs. The field names must exist * as top-level fields in the fat query. These top-level fields are used to * re-fetch any data that has changed for records identified by the data IDs. * * The supplied mapping may contain multiple field names. In addition, each * field name may map to an array of data IDs if the field is plural. */ buildFragmentForFields: function buildFragmentForFields(_ref) { var tracker = _ref.tracker; var fatQuery = _ref.fatQuery; var fieldIDs = _ref.fieldIDs; var queryTracker = tracker || RelayStoreData.getDefaultInstance().getQueryTracker(); var mutatedFields = []; forEachObject(fieldIDs, function (dataIDOrIDs, fieldName) { var fatField = getFieldFromFatQuery(fatQuery, fieldName); var dataIDs = [].concat(dataIDOrIDs); var trackedChildren = []; dataIDs.forEach(function (dataID) { trackedChildren.push.apply(trackedChildren, _toConsumableArray(queryTracker.getTrackedChildrenForID(dataID))); }); var trackedField = fatField.clone(trackedChildren); if (trackedField) { var mutationField = intersectRelayQuery(trackedField, fatField); if (mutationField) { mutatedFields.push(mutationField); } } }); return buildMutationFragment(fatQuery, mutatedFields); }, /** * Creates a fragment used to update any data as a result of a mutation that * deletes an edge from a connection. The primary difference between this and * `createForFields` is whether or not the connection edges are re-fetched. * * `connectionName` * Name of the connection field from which the edge is being deleted. * * `parentID` * ID of the parent record containing the connection which may have metadata * that needs to be re-fetched. * * `parentName` * Name of the top-level field in the fat query that corresponds to the * parent record. */ buildFragmentForEdgeDeletion: function buildFragmentForEdgeDeletion(_ref2) { var tracker = _ref2.tracker; var fatQuery = _ref2.fatQuery; var connectionName = _ref2.connectionName; var parentID = _ref2.parentID; var parentName = _ref2.parentName; tracker = tracker || RelayStoreData.getDefaultInstance().getQueryTracker(); var fatParent = getFieldFromFatQuery(fatQuery, parentName); var mutatedFields = []; var trackedParent = fatParent.clone(tracker.getTrackedChildrenForID(parentID)); if (trackedParent) { var filterUnterminatedRange = function filterUnterminatedRange(node) { return node.getSchemaName() === connectionName; }; var mutatedField = intersectRelayQuery(trackedParent, fatParent, filterUnterminatedRange); if (mutatedField) { mutatedFields.push(mutatedField); } } return buildMutationFragment(fatQuery, mutatedFields); }, /** * Creates a fragment used to fetch data necessary to insert a new edge into * an existing connection. * * `connectionName` * Name of the connection field into which the edge is being inserted. * * `parentID` * ID of the parent record containing the connection which may have metadata * that needs to be re-fetched. * * `edgeName` * Name of the top-level field in the fat query that corresponds to the * newly inserted edge. * * `parentName` * Name of the top-level field in the fat query that corresponds to the * parent record. If not supplied, metadata on the parent record and any * connections without entries in `rangeBehaviors` will not be updated. */ buildFragmentForEdgeInsertion: function buildFragmentForEdgeInsertion(_ref3) { var tracker = _ref3.tracker; var fatQuery = _ref3.fatQuery; var connectionName = _ref3.connectionName; var parentID = _ref3.parentID; var edgeName = _ref3.edgeName; var parentName = _ref3.parentName; var rangeBehaviors = _ref3.rangeBehaviors; tracker = tracker || RelayStoreData.getDefaultInstance().getQueryTracker(); var trackedChildren = tracker.getTrackedChildrenForID(parentID); var mutatedFields = []; var trackedConnections = trackedChildren.filter(function (trackedChild) { return trackedChild instanceof RelayQuery.Field && trackedChild.getSchemaName() === connectionName; }); // $FlowIssue if (trackedConnections.length) { var keysWithoutRangeBehavior = {}; var mutatedEdgeFields = []; trackedConnections.forEach(function (trackedConnection) { var trackedEdge = trackedConnection.getFieldByStorageKey('edges'); if (trackedEdge == null) { return; } if (getRangeBehaviorKey(trackedConnection) in rangeBehaviors) { // Include edges from all connections that exist in `rangeBehaviors`. // This may add duplicates, but they will eventually be flattened. mutatedEdgeFields.push.apply(mutatedEdgeFields, _toConsumableArray(trackedEdge.getChildren())); } else { // If the connection is not in `rangeBehaviors`, re-fetch it. var key = trackedConnection.getSerializationKey(); keysWithoutRangeBehavior[key] = true; } }); if (mutatedEdgeFields.length) { mutatedFields.push(buildEdgeField(parentID, edgeName, mutatedEdgeFields)); } // TODO: Do this even if there are no tracked connections. if (parentName != null) { var fatParent = getFieldFromFatQuery(fatQuery, parentName); var trackedParent = fatParent.clone(trackedChildren); if (trackedParent) { var filterUnterminatedRange = function filterUnterminatedRange(node) { return !keysWithoutRangeBehavior.hasOwnProperty(node.getSerializationKey()); }; var mutatedParent = intersectRelayQuery(trackedParent, fatParent, filterUnterminatedRange); if (mutatedParent) { mutatedFields.push(mutatedParent); } } } } return buildMutationFragment(fatQuery, mutatedFields); }, /** * Creates a fragment used to fetch the given optimistic response. */ buildFragmentForOptimisticUpdate: function buildFragmentForOptimisticUpdate(_ref4) { var response = _ref4.response; var fatQuery = _ref4.fatQuery; // Silences RelayQueryNode being incompatible with sub-class RelayQueryField // A detailed error description is available in #7635477 var mutatedFields = inferRelayFieldsFromData(response); return buildMutationFragment(fatQuery, mutatedFields); }, /** * Creates a RelayQuery.Mutation used to fetch the given optimistic response. */ buildQueryForOptimisticUpdate: function buildQueryForOptimisticUpdate(_ref5) { var response = _ref5.response; var fatQuery = _ref5.fatQuery; var mutation = _ref5.mutation; var children = [nullthrows(RelayMutationQuery.buildFragmentForOptimisticUpdate({ response: response, fatQuery: fatQuery }))]; // TODO: Can this method ever return null? Task #7705258. return nullthrows(flattenRelayQuery(RelayQuery.Node.buildMutation('OptimisticQuery', fatQuery.getType(), mutation.calls[0].name, null, children))); }, /** * Creates a RelayQuery.Mutation for the given config. See type * `MutationConfig` and the `buildFragmentForEdgeInsertion`, * `buildFragmentForEdgeDeletion` and `buildFragmentForFields` methods above * for possible configs. */ buildQuery: function buildQuery(_ref6) { var configs = _ref6.configs; var fatQuery = _ref6.fatQuery; var mutationName = _ref6.mutationName; var mutation = _ref6.mutation; var tracker = _ref6.tracker; return (function () { tracker = tracker || RelayStoreData.getDefaultInstance().getQueryTracker(); var children = [RelayQuery.Node.buildField(CLIENT_MUTATION_ID, null, null, { 'requisite': true })]; configs.forEach(function (config) { switch (config.type) { case RelayMutationType.REQUIRED_CHILDREN: children = children.concat(config.children.map(function (child) { return RelayQuery.Node.create(child, RelayMetaRoute.get('$buildQuery'), {}); })); break; case RelayMutationType.RANGE_ADD: children.push(RelayMutationQuery.buildFragmentForEdgeInsertion({ connectionName: config.connectionName, edgeName: config.edgeName, fatQuery: fatQuery, parentID: config.parentID, parentName: config.parentName, rangeBehaviors: config.rangeBehaviors, tracker: tracker })); break; case RelayMutationType.RANGE_DELETE: case RelayMutationType.NODE_DELETE: children.push(RelayMutationQuery.buildFragmentForEdgeDeletion({ connectionName: config.connectionName, fatQuery: fatQuery, parentID: config.parentID, parentName: config.parentName, tracker: tracker })); children.push(RelayQuery.Node.buildField(config.deletedIDFieldName)); break; case RelayMutationType.FIELDS_CHANGE: children.push(RelayMutationQuery.buildFragmentForFields({ fatQuery: fatQuery, fieldIDs: config.fieldIDs, tracker: tracker })); break; } }); // TODO: Can this method ever return null? Task #7705258. return nullthrows(flattenRelayQuery(RelayQuery.Node.buildMutation(mutationName, fatQuery.getType(), mutation.calls[0].name, null, children, mutation.metadata))); })(); } }; function getFieldFromFatQuery(fatQuery, fieldName) { var field = fatQuery.getFieldByStorageKey(fieldName); !field ? process.env.NODE_ENV !== 'production' ? invariant(false, 'RelayMutationQuery: Invalid field name on fat query, `%s`.', fieldName) : invariant(false) : undefined; return field; } function buildMutationFragment(fatQuery, fields) { // create a dummy field to re-fragment the input `fields` var fragmentedFields = fields.length ? refragmentRelayQuery(RelayQuery.Node.buildField('build_mutation_fragment', null, fields)) : null; var fragment = RelayQuery.Node.buildFragment('MutationQuery', fatQuery.getType(), fragmentedFields ? fragmentedFields.getChildren() : null); if (fragment) { !(fragment instanceof RelayQuery.Fragment) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'RelayMutationQuery: Expected a fragment.') : invariant(false) : undefined; return fragment; } return null; } function buildEdgeField(parentID, edgeName, edgeFields) { var fields = [RelayQuery.Node.buildField('cursor')]; if (RelayConnectionInterface.EDGES_HAVE_SOURCE_FIELD && !GraphQLStoreDataHandler.isClientID(parentID)) { fields.push(RelayQuery.Node.buildField('source', null, [RelayQuery.Node.buildField('id')])); } fields.push.apply(fields, edgeFields); return RelayQuery.Node.buildField(edgeName, null, fields); } function getRangeBehaviorKey(connectionField) { // TODO: Replace `rangeBehavior` keys with `getStorageKey()`. return connectionField.getStorageKey().substr(connectionField.getSchemaName().length + 1); } module.exports = RelayMutationQuery;