graphql
Version:
A Query Language and Runtime which can target any service.
275 lines (227 loc) • 8.77 kB
JavaScript
// Flow currently disabled for this file.
/**
* Copyright (c) 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.
*/
'use strict';
var _createClass = require('babel-runtime/helpers/create-class')['default'];
var _classCallCheck = require('babel-runtime/helpers/class-call-check')['default'];
var _slicedToArray = require('babel-runtime/helpers/sliced-to-array')['default'];
var _Object$defineProperty = require('babel-runtime/core-js/object/define-property')['default'];
var _Object$keys = require('babel-runtime/core-js/object/keys')['default'];
var _Map = require('babel-runtime/core-js/map')['default'];
var _Set = require('babel-runtime/core-js/set')['default'];
var _interopRequireDefault = require('babel-runtime/helpers/interop-require-default')['default'];
_Object$defineProperty(exports, '__esModule', {
value: true
});
exports['default'] = OverlappingFieldsCanBeMerged;
// Field name and reason, or field name and list of sub-conflicts.
var _error = require('../../error');
var _errors = require('../errors');
var _languagePrinter = require('../../language/printer');
var _languageKinds = require('../../language/kinds');
var _utilsTypeFromAST = require('../../utils/typeFromAST');
var _utilsTypeFromAST2 = _interopRequireDefault(_utilsTypeFromAST);
var _utilsFind = require('../../utils/find');
var _utilsFind2 = _interopRequireDefault(_utilsFind);
/**
* Overlapping fields can be merged
*
* A selection set is only valid if all fields (including spreading any
* fragments) either correspond to distinct response names or can be merged
* without ambiguity.
*/
function OverlappingFieldsCanBeMerged(context) {
var comparedSet = new PairSet();
function findConflicts(fieldMap) {
var conflicts = [];
_Object$keys(fieldMap).forEach(function (responseName) {
var fields = fieldMap[responseName];
if (fields.length > 1) {
for (var i = 0; i < fields.length; i++) {
for (var j = i; j < fields.length; j++) {
var conflict = findConflict(responseName, fields[i], fields[j]);
if (conflict) {
conflicts.push(conflict);
}
}
}
}
});
return conflicts;
}
function findConflict(responseName, pair1, pair2) {
var _pair1 = _slicedToArray(pair1, 2);
var ast1 = _pair1[0];
var def1 = _pair1[1];
var _pair2 = _slicedToArray(pair2, 2);
var ast2 = _pair2[0];
var def2 = _pair2[1];
if (ast1 === ast2 || comparedSet.has(ast1, ast2)) {
return;
}
comparedSet.add(ast1, ast2);
var name1 = ast1.name.value;
var name2 = ast2.name.value;
if (name1 !== name2) {
return [[responseName, '' + name1 + ' and ' + name2 + ' are different fields'], [ast1, ast2]];
}
var type1 = def1 && def1.type;
var type2 = def2 && def2.type;
if (!sameType(type1, type2)) {
return [[responseName, 'they return differing types ' + type1 + ' and ' + type2], [ast1, ast2]];
}
var args1 = ast1.arguments || [];
var args2 = ast2.arguments || [];
if (!sameNameValuePairs(args1, args2)) {
return [[responseName, 'they have differing arguments'], [ast1, ast2]];
}
var directives1 = ast1.directives || [];
var directives2 = ast2.directives || [];
if (!sameNameValuePairs(directives1, directives2)) {
return [[responseName, 'they have differing directives'], [ast1, ast2]];
}
var selectionSet1 = ast1.selectionSet;
var selectionSet2 = ast2.selectionSet;
if (selectionSet1 && selectionSet2) {
var visitedFragmentNames = {};
var subfieldMap = collectFieldASTsAndDefs(context, type1, selectionSet1, visitedFragmentNames);
subfieldMap = collectFieldASTsAndDefs(context, type2, selectionSet2, visitedFragmentNames, subfieldMap);
var conflicts = findConflicts(subfieldMap);
if (conflicts.length > 0) {
return [[responseName, conflicts.map(function (_ref) {
var _ref2 = _slicedToArray(_ref, 1);
var reason = _ref2[0];
return reason;
})], conflicts.reduce(function (list, _ref3) {
var _ref32 = _slicedToArray(_ref3, 2);
var blameNodes = _ref32[1];
return list.concat(blameNodes);
}, [ast1, ast2])];
}
}
}
return {
SelectionSet: {
// Note: we validate on the reverse traversal so deeper conflicts will be
// caught first, for clearer error messages.
leave: function leave(selectionSet) {
var fieldMap = collectFieldASTsAndDefs(context, context.getType(), selectionSet);
var conflicts = findConflicts(fieldMap);
if (conflicts.length) {
return conflicts.map(function (_ref4) {
var _ref42 = _slicedToArray(_ref4, 2);
var _ref42$0 = _slicedToArray(_ref42[0], 2);
var responseName = _ref42$0[0];
var reason = _ref42$0[1];
var blameNodes = _ref42[1];
return new _error.GraphQLError((0, _errors.fieldsConflictMessage)(responseName, reason), blameNodes);
});
}
}
}
};
}
function sameNameValuePairs(pairs1, pairs2) {
if (pairs1.length !== pairs2.length) {
return false;
}
return pairs1.every(function (pair1) {
var pair2 = (0, _utilsFind2['default'])(pairs2, function (pair) {
return pair.name.value === pair1.name.value;
});
if (!pair2) {
return false;
}
return sameValue(pair1.value, pair2.value);
});
}
function sameValue(value1, value2) {
return !value1 && !value2 || (0, _languagePrinter.print)(value1) === (0, _languagePrinter.print)(value2);
}
function sameType(type1, type2) {
return !type1 && !type2 || String(type1) === String(type2);
}
/**
* Given a selectionSet, adds all of the fields in that selection to
* the passed in map of fields, and returns it at the end.
*
* Note: This is not the same as execution's collectFields because at static
* time we do not know what object type will be used, so we unconditionally
* spread in all fragments.
*/
function collectFieldASTsAndDefs(context, parentType, selectionSet, visitedFragmentNames, astAndDefs) {
var _visitedFragmentNames = visitedFragmentNames || {};
var _astAndDefs = astAndDefs || {};
for (var i = 0; i < selectionSet.selections.length; i++) {
var selection = selectionSet.selections[i];
switch (selection.kind) {
case _languageKinds.FIELD:
var fieldAST = selection;
var fieldName = fieldAST.name.value;
var fieldDef = parentType && parentType.getFields && parentType.getFields()[fieldName];
var responseName = fieldAST.alias ? fieldAST.alias.value : fieldName;
if (!_astAndDefs[responseName]) {
_astAndDefs[responseName] = [];
}
_astAndDefs[responseName].push([fieldAST, fieldDef]);
break;
case _languageKinds.INLINE_FRAGMENT:
var inlineFragment = selection;
_astAndDefs = collectFieldASTsAndDefs(context, (0, _utilsTypeFromAST2['default'])(context.getSchema(), inlineFragment.typeCondition), inlineFragment.selectionSet, _visitedFragmentNames, _astAndDefs);
break;
case _languageKinds.FRAGMENT_SPREAD:
var fragmentSpread = selection;
var fragName = fragmentSpread.name.value;
if (_visitedFragmentNames[fragName]) {
continue;
}
_visitedFragmentNames[fragName] = true;
var fragment = context.getFragment(fragName);
if (!fragment) {
continue;
}
_astAndDefs = collectFieldASTsAndDefs(context, (0, _utilsTypeFromAST2['default'])(context.getSchema(), fragment.typeCondition), fragment.selectionSet, _visitedFragmentNames, _astAndDefs);
break;
}
}
return _astAndDefs;
}
/**
* A way to keep track of pairs of things when the ordering of the pair does
* not matter. We do this by maintaining a sort of double adjacency sets.
*/
var PairSet = (function () {
function PairSet() {
_classCallCheck(this, PairSet);
this._data = new _Map();
}
_createClass(PairSet, [{
key: 'has',
value: function has(a, b) {
var first = this._data.get(a);
return first && first.has(b);
}
}, {
key: 'add',
value: function add(a, b) {
_pairSetAdd(this._data, a, b);
_pairSetAdd(this._data, b, a);
}
}]);
return PairSet;
})();
function _pairSetAdd(data, a, b) {
var set = data.get(a);
if (!set) {
set = new _Set();
data.set(a, set);
}
set.add(b);
}
module.exports = exports['default'];