270 lines
9.7 KiB
JavaScript
270 lines
9.7 KiB
JavaScript
/**
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @format
|
|
*
|
|
*/
|
|
'use strict';
|
|
|
|
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
|
|
|
|
var _objectSpread2 = _interopRequireDefault(require("@babel/runtime/helpers/objectSpread"));
|
|
|
|
var GraphQLCompilerContext = require("./GraphQLCompilerContext");
|
|
|
|
var GraphQLIRTransformer = require("./GraphQLIRTransformer");
|
|
|
|
var GraphQLSchemaUtils = require("./GraphQLSchemaUtils");
|
|
|
|
var areEqual = require("./areEqualOSS");
|
|
|
|
var getIdentifierForSelection = require("./getIdentifierForSelection");
|
|
|
|
var _require = require("./RelayCompilerError"),
|
|
createUserError = _require.createUserError,
|
|
createCompilerError = _require.createCompilerError;
|
|
|
|
var getRawType = GraphQLSchemaUtils.getRawType,
|
|
isAbstractType = GraphQLSchemaUtils.isAbstractType;
|
|
|
|
/**
|
|
* Transform that flattens inline fragments, fragment spreads, and conditionals.
|
|
*
|
|
* Inline fragments are inlined (replaced with their selections) when:
|
|
* - The fragment type matches the type of its parent.
|
|
* - The fragment has an abstract type and the `flattenAbstractTypes` option has
|
|
* been set.
|
|
* - The 'flattenInlineFragments' option has been set.
|
|
*/
|
|
function flattenTransformImpl(context, options) {
|
|
var state = {
|
|
flattenAbstractTypes: !!(options && options.flattenAbstractTypes),
|
|
flattenInlineFragments: !!(options && options.flattenInlineFragments),
|
|
parentType: null
|
|
};
|
|
return GraphQLIRTransformer.transform(context, {
|
|
Root: flattenSelections,
|
|
Fragment: flattenSelections,
|
|
Condition: flattenSelections,
|
|
InlineFragment: flattenSelections,
|
|
LinkedField: flattenSelections,
|
|
MatchField: flattenSelections
|
|
}, function () {
|
|
return state;
|
|
});
|
|
}
|
|
/**
|
|
* @private
|
|
*/
|
|
|
|
|
|
function flattenSelections(node, state) {
|
|
// Determine the current type.
|
|
var parentType = state.parentType;
|
|
var type = node.kind === 'Condition' || node.kind === 'Defer' || node.kind === 'Stream' ? parentType : node.kind === 'InlineFragment' ? node.typeCondition : node.type;
|
|
|
|
if (type == null) {
|
|
throw createCompilerError('FlattenTransform: Expected a parent type.', [node.loc]);
|
|
} // Flatten the selections in this node, creating a new node with flattened
|
|
// selections if possible, then deeply traverse the flattened node, while
|
|
// keeping track of the parent type.
|
|
|
|
|
|
var nextSelections = new Map();
|
|
var hasFlattened = flattenSelectionsInto(nextSelections, node, state, type);
|
|
var flattenedNode = hasFlattened ? (0, _objectSpread2["default"])({}, node, {
|
|
selections: Array.from(nextSelections.values())
|
|
}) : node;
|
|
state.parentType = type;
|
|
var deeplyFlattenedNode = this.traverse(flattenedNode, state);
|
|
state.parentType = parentType;
|
|
return deeplyFlattenedNode;
|
|
}
|
|
/**
|
|
* @private
|
|
*/
|
|
|
|
|
|
function flattenSelectionsInto(flattenedSelections, node, state, type) {
|
|
var hasFlattened = false;
|
|
node.selections.forEach(function (selection) {
|
|
if (selection.kind === 'InlineFragment' && shouldFlattenInlineFragment(selection, state, type)) {
|
|
hasFlattened = true;
|
|
flattenSelectionsInto(flattenedSelections, selection, state, type);
|
|
return;
|
|
}
|
|
|
|
var nodeIdentifier = getIdentifierForSelection(selection);
|
|
var flattenedSelection = flattenedSelections.get(nodeIdentifier); // If this selection hasn't been seen before, keep track of it.
|
|
|
|
if (!flattenedSelection) {
|
|
flattenedSelections.set(nodeIdentifier, selection);
|
|
return;
|
|
} // Otherwise a similar selection exists which should be merged.
|
|
|
|
|
|
hasFlattened = true;
|
|
|
|
if (flattenedSelection.kind === 'InlineFragment') {
|
|
if (selection.kind !== 'InlineFragment') {
|
|
throw createCompilerError("FlattenTransform: Expected an InlineFragment, got a '".concat(selection.kind, "'"), [selection.loc]);
|
|
}
|
|
|
|
flattenedSelections.set(nodeIdentifier, (0, _objectSpread2["default"])({}, flattenedSelection, {
|
|
selections: mergeSelections(flattenedSelection, selection, state, selection.typeCondition)
|
|
}));
|
|
} else if (flattenedSelection.kind === 'Condition') {
|
|
if (selection.kind !== 'Condition') {
|
|
throw createCompilerError("FlattenTransform: Expected a Condition, got a '".concat(selection.kind, "'"), [selection.loc]);
|
|
}
|
|
|
|
flattenedSelections.set(nodeIdentifier, (0, _objectSpread2["default"])({}, flattenedSelection, {
|
|
selections: mergeSelections(flattenedSelection, selection, state, type)
|
|
}));
|
|
} else if (flattenedSelection.kind === 'FragmentSpread') {// Ignore duplicate fragment spreads.
|
|
} else if (flattenedSelection.kind === 'MatchField' || flattenedSelection.kind === 'MatchBranch') {// Ignore duplicate matches that select the same fragments and
|
|
// modules (encoded in the identifier)
|
|
// Also ignore incremental data placeholders
|
|
} else if (flattenedSelection.kind === 'Defer') {
|
|
if (selection.kind !== 'Defer') {
|
|
throw createCompilerError("FlattenTransform: Expected a Defer, got a '".concat(selection.kind, "'"), [selection.loc]);
|
|
}
|
|
|
|
flattenedSelections.set(nodeIdentifier, (0, _objectSpread2["default"])({
|
|
kind: 'Defer'
|
|
}, flattenedSelection, {
|
|
selections: mergeSelections(flattenedSelection, selection, state, type)
|
|
}));
|
|
} else if (flattenedSelection.kind === 'Stream') {
|
|
if (selection.kind !== 'Stream') {
|
|
throw createCompilerError("FlattenTransform: Expected a Stream, got a '".concat(selection.kind, "'"), [selection.loc]);
|
|
}
|
|
|
|
flattenedSelections.set(nodeIdentifier, (0, _objectSpread2["default"])({
|
|
kind: 'Stream'
|
|
}, flattenedSelection, {
|
|
selections: mergeSelections(flattenedSelection, selection, state, type)
|
|
}));
|
|
} else if (flattenedSelection.kind === 'LinkedField') {
|
|
if (selection.kind !== 'LinkedField') {
|
|
throw createCompilerError("FlattenTransform: Expected a LinkedField, got a '".concat(selection.kind, "'"), [selection.loc]);
|
|
} // Note: arguments are intentionally reversed to avoid rebuilds
|
|
|
|
|
|
assertUniqueArgsForAlias(selection, flattenedSelection);
|
|
flattenedSelections.set(nodeIdentifier, (0, _objectSpread2["default"])({
|
|
kind: 'LinkedField'
|
|
}, flattenedSelection, {
|
|
handles: mergeHandles(flattenedSelection, selection),
|
|
selections: mergeSelections(flattenedSelection, selection, state, selection.type)
|
|
}));
|
|
} else if (flattenedSelection.kind === 'ScalarField') {
|
|
if (selection.kind !== 'ScalarField') {
|
|
throw createCompilerError("FlattenTransform: Expected a ScalarField, got a '".concat(selection.kind, "'"), [selection.loc]);
|
|
} // Note: arguments are intentionally reversed to avoid rebuilds
|
|
|
|
|
|
assertUniqueArgsForAlias(selection, flattenedSelection);
|
|
flattenedSelections.set(nodeIdentifier, (0, _objectSpread2["default"])({
|
|
kind: 'ScalarField'
|
|
}, flattenedSelection, {
|
|
// Note: arguments are intentionally reversed to avoid rebuilds
|
|
handles: mergeHandles(selection, flattenedSelection)
|
|
}));
|
|
} else {
|
|
flattenedSelection.kind;
|
|
throw createCompilerError("FlattenTransform: Unknown kind '".concat(flattenedSelection.kind, "'"));
|
|
}
|
|
});
|
|
return hasFlattened;
|
|
}
|
|
/**
|
|
* @private
|
|
*/
|
|
|
|
|
|
function mergeSelections(nodeA, nodeB, state, type) {
|
|
var flattenedSelections = new Map();
|
|
flattenSelectionsInto(flattenedSelections, nodeA, state, type);
|
|
flattenSelectionsInto(flattenedSelections, nodeB, state, type);
|
|
return Array.from(flattenedSelections.values());
|
|
}
|
|
/**
|
|
* @private
|
|
* TODO(T19327202) This is redundant with OverlappingFieldsCanBeMergedRule once
|
|
* it can be enabled.
|
|
*/
|
|
|
|
|
|
function assertUniqueArgsForAlias(field, otherField) {
|
|
if (!areEqualFields(field, otherField)) {
|
|
var _field$alias;
|
|
|
|
throw createUserError('Expected all fields on the same parent with ' + "the name or alias '".concat((_field$alias = field.alias) !== null && _field$alias !== void 0 ? _field$alias : field.name, "' to have the same name and arguments."), [field.loc, otherField.loc]);
|
|
}
|
|
}
|
|
/**
|
|
* @private
|
|
*/
|
|
|
|
|
|
function shouldFlattenInlineFragment(fragment, state, type) {
|
|
return state.flattenInlineFragments || fragment.typeCondition.name === getRawType(type).name || state.flattenAbstractTypes && isAbstractType(fragment.typeCondition);
|
|
}
|
|
/**
|
|
* @private
|
|
*
|
|
* Verify that two fields are equal in all properties other than their
|
|
* selections.
|
|
*/
|
|
|
|
|
|
function areEqualFields(thisField, thatField) {
|
|
return thisField.kind === thatField.kind && thisField.name === thatField.name && thisField.alias === thatField.alias && areEqualArgs(thisField.args, thatField.args);
|
|
}
|
|
/**
|
|
* Verify that two sets of arguments are equivalent - same argument names
|
|
* and values. Notably this ignores the types of arguments and values, which
|
|
* may not always be inferred identically.
|
|
*/
|
|
|
|
|
|
function areEqualArgs(thisArgs, thatArgs) {
|
|
return thisArgs.length === thatArgs.length && thisArgs.every(function (thisArg, index) {
|
|
var thatArg = thatArgs[index];
|
|
return thisArg.name === thatArg.name && thisArg.value.kind === thatArg.value.kind && thisArg.value.variableName === thatArg.value.variableName && areEqual(thisArg.value.value, thatArg.value.value);
|
|
});
|
|
}
|
|
/**
|
|
* @private
|
|
*/
|
|
|
|
|
|
function mergeHandles(nodeA, nodeB) {
|
|
if (!nodeA.handles) {
|
|
return nodeB.handles;
|
|
}
|
|
|
|
if (!nodeB.handles) {
|
|
return nodeA.handles;
|
|
}
|
|
|
|
var uniqueItems = new Map();
|
|
nodeA.handles.concat(nodeB.handles).forEach(function (item) {
|
|
return uniqueItems.set(item.name + item.key, item);
|
|
});
|
|
return Array.from(uniqueItems.values());
|
|
}
|
|
|
|
function transformWithOptions(options) {
|
|
return function flattenTransform(context) {
|
|
return flattenTransformImpl(context, options);
|
|
};
|
|
}
|
|
|
|
module.exports = {
|
|
transformWithOptions: transformWithOptions
|
|
}; |