@graphql-tools/delegate
Version:
A set of utils for faster development of GraphQL tools
424 lines (423 loc) • 19.7 kB
JavaScript
import { getNamedType, isAbstractType, isInterfaceType, isLeafType, isObjectType, Kind, TypeInfo, visit, visitWithTypeInfo, } from 'graphql';
import { getRootTypeNames, implementsAbstractType, memoize2, } from '@graphql-tools/utils';
import { extractUnavailableFields } from './extractUnavailableFields.js';
import { getDocumentMetadata } from './getDocumentMetadata.js';
export function prepareGatewayDocument(originalDocument, transformedSchema, returnType, infoSchema) {
let wrappedConcreteTypesDocument = wrapConcreteTypes(returnType, transformedSchema, originalDocument);
if (infoSchema == null) {
return wrappedConcreteTypesDocument;
}
const visitedSelections = new WeakSet();
wrappedConcreteTypesDocument = visit(wrappedConcreteTypesDocument, {
[Kind.SELECTION_SET](node) {
const newSelections = [];
for (const selectionNode of node.selections) {
if (selectionNode.kind === Kind.INLINE_FRAGMENT &&
selectionNode.typeCondition != null &&
!visitedSelections.has(selectionNode)) {
visitedSelections.add(selectionNode);
const typeName = selectionNode.typeCondition.name.value;
const gatewayType = infoSchema.getType(typeName);
const subschemaType = transformedSchema.getType(typeName);
if (isAbstractType(gatewayType)) {
const possibleTypes = infoSchema.getPossibleTypes(gatewayType);
if (isAbstractType(subschemaType)) {
const possibleTypesInSubschema = transformedSchema.getPossibleTypes(subschemaType);
const extraTypesForSubschema = new Set();
for (const possibleType of possibleTypes) {
const possibleTypeInSubschema = transformedSchema.getType(possibleType.name);
// If it is a possible type in the gateway schema, it should be a possible type in the subschema
if (possibleTypeInSubschema &&
possibleTypesInSubschema.some(t => t.name === possibleType.name)) {
continue;
}
// If it doesn't exist in the subschema
if (!possibleTypeInSubschema) {
continue;
}
// If it exists in the subschema but it is not a possible type
extraTypesForSubschema.add(possibleType.name);
}
for (const extraType of extraTypesForSubschema) {
newSelections.push({
...selectionNode,
typeCondition: {
kind: Kind.NAMED_TYPE,
name: {
kind: Kind.NAME,
value: extraType,
},
},
});
}
}
}
const typeInSubschema = transformedSchema.getType(typeName);
if (!typeInSubschema) {
for (const selection of selectionNode.selectionSet.selections) {
newSelections.push(selection);
}
}
if (typeInSubschema && 'getFields' in typeInSubschema) {
const fieldMap = typeInSubschema.getFields();
for (const selection of selectionNode.selectionSet.selections) {
if (selection.kind === Kind.FIELD) {
const fieldName = selection.name.value;
const field = fieldMap[fieldName];
if (!field) {
newSelections.push(selection);
}
}
}
}
}
newSelections.push(selectionNode);
}
return {
...node,
selections: newSelections,
};
},
});
const { possibleTypesMap, reversePossibleTypesMap, interfaceExtensionsMap, fieldNodesByType, fieldNodesByField, dynamicSelectionSetsByField, } = getSchemaMetaData(infoSchema, transformedSchema);
const { operations, fragments, fragmentNames } = getDocumentMetadata(wrappedConcreteTypesDocument);
const { expandedFragments, fragmentReplacements } = getExpandedFragments(fragments, fragmentNames, possibleTypesMap);
const typeInfo = new TypeInfo(transformedSchema);
const expandedDocument = {
kind: Kind.DOCUMENT,
definitions: [...operations, ...fragments, ...expandedFragments],
};
const visitorKeyMap = {
Document: ['definitions'],
OperationDefinition: ['selectionSet'],
SelectionSet: ['selections'],
Field: ['selectionSet'],
InlineFragment: ['selectionSet'],
FragmentDefinition: ['selectionSet'],
};
return visit(expandedDocument, visitWithTypeInfo(typeInfo, {
[Kind.SELECTION_SET]: node => visitSelectionSet(node, fragmentReplacements, transformedSchema, typeInfo, possibleTypesMap, reversePossibleTypesMap, interfaceExtensionsMap, fieldNodesByType, fieldNodesByField, dynamicSelectionSetsByField),
}),
// visitorKeys argument usage a la https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby-source-graphql/src/batching/merge-queries.js
// empty keys cannot be removed only because of typescript errors
// will hopefully be fixed in future version of graphql-js to be optional
visitorKeyMap);
}
const shouldAdd = () => true;
function visitSelectionSet(node, fragmentReplacements, schema, typeInfo, possibleTypesMap, reversePossibleTypesMap, interfaceExtensionsMap, fieldNodesByType, fieldNodesByField, dynamicSelectionSetsByField) {
const newSelections = new Set();
const maybeType = typeInfo.getParentType();
if (maybeType != null) {
const parentType = getNamedType(maybeType);
const parentTypeName = parentType.name;
const fieldNodes = fieldNodesByType[parentTypeName];
if (fieldNodes) {
for (const fieldNode of fieldNodes) {
newSelections.add(fieldNode);
}
}
const interfaceExtensions = interfaceExtensionsMap[parentType.name];
const interfaceExtensionFields = [];
for (const selection of node.selections) {
if (selection.kind === Kind.INLINE_FRAGMENT) {
if (selection.typeCondition != null) {
const possibleTypes = possibleTypesMap[selection.typeCondition.name.value];
if (possibleTypes == null) {
const fieldNodesForTypeName = fieldNodesByField[parentTypeName]?.['__typename'];
if (fieldNodesForTypeName) {
for (const fieldNode of fieldNodesForTypeName) {
newSelections.add(fieldNode);
}
}
newSelections.add(selection);
continue;
}
for (const possibleTypeName of possibleTypes) {
const maybePossibleType = schema.getType(possibleTypeName);
if (maybePossibleType != null &&
implementsAbstractType(schema, parentType, maybePossibleType)) {
newSelections.add(generateInlineFragment(possibleTypeName, selection.selectionSet));
}
}
if (possibleTypes.length === 0) {
newSelections.add(selection);
}
}
else {
newSelections.add(selection);
}
}
else if (selection.kind === Kind.FRAGMENT_SPREAD) {
const fragmentName = selection.name.value;
if (!fragmentReplacements[fragmentName]) {
newSelections.add(selection);
continue;
}
for (const replacement of fragmentReplacements[fragmentName]) {
const typeName = replacement.typeName;
const maybeReplacementType = schema.getType(typeName);
if (maybeReplacementType != null &&
implementsAbstractType(schema, parentType, maybeType)) {
newSelections.add({
kind: Kind.FRAGMENT_SPREAD,
name: {
kind: Kind.NAME,
value: replacement.fragmentName,
},
});
}
}
}
else {
const fieldName = selection.name.value;
let skipAddingDependencyNodes = false;
// TODO: Optimization to prevent extra fields to the subgraph
if (isAbstractType(parentType)) {
skipAddingDependencyNodes = false;
const fieldNodesForTypeName = fieldNodesByField[parentTypeName]?.['__typename'];
if (fieldNodesForTypeName) {
for (const fieldNode of fieldNodesForTypeName) {
newSelections.add(fieldNode);
}
}
}
else if (isObjectType(parentType) || isInterfaceType(parentType)) {
const fieldMap = parentType.getFields();
const field = fieldMap[fieldName];
if (field) {
const unavailableFields = extractUnavailableFields(schema, field, selection, shouldAdd);
skipAddingDependencyNodes = unavailableFields.length === 0;
}
}
if (!skipAddingDependencyNodes) {
const fieldNodes = fieldNodesByField[parentTypeName]?.[fieldName];
if (fieldNodes != null) {
for (const fieldNode of fieldNodes) {
newSelections.add(fieldNode);
}
}
const dynamicSelectionSets = dynamicSelectionSetsByField[parentTypeName]?.[fieldName];
if (dynamicSelectionSets != null) {
for (const selectionSetFn of dynamicSelectionSets) {
const selectionSet = selectionSetFn(selection);
if (selectionSet != null) {
for (const selection of selectionSet.selections) {
newSelections.add(selection);
}
}
}
}
}
if (interfaceExtensions?.[fieldName]) {
interfaceExtensionFields.push(selection);
}
else {
newSelections.add(selection);
}
}
}
if (reversePossibleTypesMap[parentType.name]) {
newSelections.add({
kind: Kind.FIELD,
name: {
kind: Kind.NAME,
value: '__typename',
},
});
}
if (interfaceExtensionFields.length) {
const possibleTypes = possibleTypesMap[parentType.name];
if (possibleTypes != null) {
for (const possibleType of possibleTypes) {
newSelections.add(generateInlineFragment(possibleType, {
kind: Kind.SELECTION_SET,
selections: interfaceExtensionFields,
}));
}
}
}
return {
...node,
selections: Array.from(newSelections),
};
}
return node;
}
function generateInlineFragment(typeName, selectionSet) {
return {
kind: Kind.INLINE_FRAGMENT,
typeCondition: {
kind: Kind.NAMED_TYPE,
name: {
kind: Kind.NAME,
value: typeName,
},
},
selectionSet,
};
}
const getSchemaMetaData = memoize2((sourceSchema, targetSchema) => {
const typeMap = sourceSchema.getTypeMap();
const targetTypeMap = targetSchema.getTypeMap();
const possibleTypesMap = Object.create(null);
const interfaceExtensionsMap = Object.create(null);
for (const typeName in typeMap) {
const type = typeMap[typeName];
if (isAbstractType(type)) {
const targetType = targetTypeMap[typeName];
if (isInterfaceType(type) && isInterfaceType(targetType)) {
const targetTypeFields = targetType.getFields();
const sourceTypeFields = type.getFields();
const extensionFields = Object.create(null);
let isExtensionFieldsEmpty = true;
for (const fieldName in sourceTypeFields) {
if (!targetTypeFields[fieldName]) {
extensionFields[fieldName] = true;
isExtensionFieldsEmpty = false;
}
}
if (!isExtensionFieldsEmpty) {
interfaceExtensionsMap[typeName] = extensionFields;
}
}
if (interfaceExtensionsMap[typeName] || !isAbstractType(targetType)) {
const implementations = sourceSchema.getPossibleTypes(type);
possibleTypesMap[typeName] = [];
for (const impl of implementations) {
if (targetTypeMap[impl.name]) {
possibleTypesMap[typeName].push(impl.name);
}
}
}
}
}
const stitchingInfo = sourceSchema.extensions?.['stitchingInfo'];
return {
possibleTypesMap,
reversePossibleTypesMap: reversePossibleTypesMap(possibleTypesMap),
interfaceExtensionsMap,
fieldNodesByType: stitchingInfo?.fieldNodesByType ?? {},
fieldNodesByField: stitchingInfo?.fieldNodesByField ?? {},
dynamicSelectionSetsByField: stitchingInfo?.dynamicSelectionSetsByField ?? {},
};
});
function reversePossibleTypesMap(possibleTypesMap) {
const result = Object.create(null);
for (const typeName in possibleTypesMap) {
const toTypeNames = possibleTypesMap[typeName];
for (const toTypeName of toTypeNames) {
if (!result[toTypeName]) {
result[toTypeName] = [];
}
result[toTypeName].push(typeName);
}
}
return result;
}
function getExpandedFragments(fragments, fragmentNames, possibleTypesMap) {
let fragmentCounter = 0;
function generateFragmentName(typeName) {
let fragmentName;
do {
fragmentName = `_${typeName}_Fragment${fragmentCounter.toString()}`;
fragmentCounter++;
} while (fragmentNames.has(fragmentName));
return fragmentName;
}
const expandedFragments = [];
const fragmentReplacements = Object.create(null);
for (const fragment of fragments) {
const possibleTypes = possibleTypesMap[fragment.typeCondition.name.value];
if (possibleTypes != null) {
const fragmentName = fragment.name.value;
fragmentReplacements[fragmentName] = [];
for (const possibleTypeName of possibleTypes) {
const name = generateFragmentName(possibleTypeName);
fragmentNames.add(name);
expandedFragments.push({
kind: Kind.FRAGMENT_DEFINITION,
name: {
kind: Kind.NAME,
value: name,
},
typeCondition: {
kind: Kind.NAMED_TYPE,
name: {
kind: Kind.NAME,
value: possibleTypeName,
},
},
selectionSet: fragment.selectionSet,
});
fragmentReplacements[fragmentName].push({
fragmentName: name,
typeName: possibleTypeName,
});
}
}
}
return {
expandedFragments,
fragmentReplacements,
};
}
function wrapConcreteTypes(returnType, targetSchema, document) {
const namedType = getNamedType(returnType);
if (isLeafType(namedType)) {
return document;
}
let possibleTypes = isAbstractType(namedType)
? targetSchema.getPossibleTypes(namedType)
: [namedType];
if (possibleTypes.length === 0) {
possibleTypes = [namedType];
}
const rootTypeNames = getRootTypeNames(targetSchema);
const typeInfo = new TypeInfo(targetSchema);
const visitorKeys = {
Document: ['definitions'],
OperationDefinition: ['selectionSet'],
SelectionSet: ['selections'],
InlineFragment: ['selectionSet'],
FragmentDefinition: ['selectionSet'],
};
return visit(document, visitWithTypeInfo(typeInfo, {
[Kind.FRAGMENT_DEFINITION]: (node) => {
const typeName = node.typeCondition.name.value;
if (!rootTypeNames.has(typeName)) {
return false;
}
},
[Kind.FIELD]: (node) => {
const fieldType = typeInfo.getType();
if (fieldType) {
const fieldNamedType = getNamedType(fieldType);
if (isAbstractType(fieldNamedType) &&
fieldNamedType.name !== namedType.name &&
possibleTypes.length > 0) {
return {
...node,
selectionSet: {
kind: Kind.SELECTION_SET,
selections: possibleTypes.map(possibleType => ({
kind: Kind.INLINE_FRAGMENT,
typeCondition: {
kind: Kind.NAMED_TYPE,
name: {
kind: Kind.NAME,
value: possibleType.name,
},
},
selectionSet: node.selectionSet,
})),
},
};
}
}
},
}),
// visitorKeys argument usage a la https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby-source-graphql/src/batching/merge-queries.js
// empty keys cannot be removed only because of typescript errors
// will hopefully be fixed in future version of graphql-js to be optional
visitorKeys);
}