UNPKG

@graphql-tools/delegate

Version:

A set of utils for faster development of GraphQL tools

367 lines (366 loc) • 16.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.prepareGatewayDocument = void 0; const graphql_1 = require("graphql"); const utils_1 = require("@graphql-tools/utils"); const extractUnavailableFields_js_1 = require("./extractUnavailableFields.js"); const getDocumentMetadata_js_1 = require("./getDocumentMetadata.js"); function prepareGatewayDocument(originalDocument, transformedSchema, returnType, infoSchema) { let wrappedConcreteTypesDocument = wrapConcreteTypes(returnType, transformedSchema, originalDocument); if (infoSchema == null) { return wrappedConcreteTypesDocument; } const visitedSelections = new WeakSet(); wrappedConcreteTypesDocument = (0, graphql_1.visit)(wrappedConcreteTypesDocument, { [graphql_1.Kind.SELECTION_SET](node) { const newSelections = []; for (const selectionNode of node.selections) { if (selectionNode.kind === graphql_1.Kind.INLINE_FRAGMENT && selectionNode.typeCondition != null && !visitedSelections.has(selectionNode)) { visitedSelections.add(selectionNode); const typeName = selectionNode.typeCondition.name.value; const type = infoSchema.getType(typeName); if ((0, graphql_1.isAbstractType)(type)) { const possibleTypes = infoSchema.getPossibleTypes(type); for (const possibleType of possibleTypes) { newSelections.push({ ...selectionNode, typeCondition: { kind: graphql_1.Kind.NAMED_TYPE, name: { kind: graphql_1.Kind.NAME, value: possibleType.name, }, }, }); } } } newSelections.push(selectionNode); } return { ...node, selections: newSelections, }; }, }); const { possibleTypesMap, reversePossibleTypesMap, interfaceExtensionsMap, fieldNodesByType, fieldNodesByField, dynamicSelectionSetsByField, } = getSchemaMetaData(infoSchema, transformedSchema); const { operations, fragments, fragmentNames } = (0, getDocumentMetadata_js_1.getDocumentMetadata)(wrappedConcreteTypesDocument); const { expandedFragments, fragmentReplacements } = getExpandedFragments(fragments, fragmentNames, possibleTypesMap); const typeInfo = new graphql_1.TypeInfo(transformedSchema); const expandedDocument = { kind: graphql_1.Kind.DOCUMENT, definitions: [...operations, ...fragments, ...expandedFragments], }; const visitorKeyMap = { Document: ['definitions'], OperationDefinition: ['selectionSet'], SelectionSet: ['selections'], Field: ['selectionSet'], InlineFragment: ['selectionSet'], FragmentDefinition: ['selectionSet'], }; return (0, graphql_1.visit)(expandedDocument, (0, graphql_1.visitWithTypeInfo)(typeInfo, { [graphql_1.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); } exports.prepareGatewayDocument = prepareGatewayDocument; 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 = (0, graphql_1.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 === graphql_1.Kind.INLINE_FRAGMENT) { if (selection.typeCondition != null) { const possibleTypes = possibleTypesMap[selection.typeCondition.name.value]; if (possibleTypes == null) { newSelections.add(selection); continue; } for (const possibleTypeName of possibleTypes) { const maybePossibleType = schema.getType(possibleTypeName); if (maybePossibleType != null && (0, utils_1.implementsAbstractType)(schema, parentType, maybePossibleType)) { newSelections.add(generateInlineFragment(possibleTypeName, selection.selectionSet)); } } } } else if (selection.kind === graphql_1.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 && (0, utils_1.implementsAbstractType)(schema, parentType, maybeType)) { newSelections.add({ kind: graphql_1.Kind.FRAGMENT_SPREAD, name: { kind: graphql_1.Kind.NAME, value: replacement.fragmentName, }, }); } } } else { const fieldName = selection.name.value; let skipAddingDependencyNodes = false; // TODO: Optimization to prevent extra fields to the subgraph if ((0, graphql_1.isObjectType)(parentType) || (0, graphql_1.isInterfaceType)(parentType)) { const fieldMap = parentType.getFields(); const field = fieldMap[fieldName]; if (field) { const unavailableFields = (0, extractUnavailableFields_js_1.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: graphql_1.Kind.FIELD, name: { kind: graphql_1.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: graphql_1.Kind.SELECTION_SET, selections: interfaceExtensionFields, })); } } } return { ...node, selections: Array.from(newSelections), }; } return node; } function generateInlineFragment(typeName, selectionSet) { return { kind: graphql_1.Kind.INLINE_FRAGMENT, typeCondition: { kind: graphql_1.Kind.NAMED_TYPE, name: { kind: graphql_1.Kind.NAME, value: typeName, }, }, selectionSet, }; } const getSchemaMetaData = (0, utils_1.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 ((0, graphql_1.isAbstractType)(type)) { const targetType = targetTypeMap[typeName]; if ((0, graphql_1.isInterfaceType)(type) && (0, graphql_1.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] || !(0, graphql_1.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: graphql_1.Kind.FRAGMENT_DEFINITION, name: { kind: graphql_1.Kind.NAME, value: name, }, typeCondition: { kind: graphql_1.Kind.NAMED_TYPE, name: { kind: graphql_1.Kind.NAME, value: possibleTypeName, }, }, selectionSet: fragment.selectionSet, }); fragmentReplacements[fragmentName].push({ fragmentName: name, typeName: possibleTypeName, }); } } } return { expandedFragments, fragmentReplacements, }; } function wrapConcreteTypes(returnType, targetSchema, document) { const namedType = (0, graphql_1.getNamedType)(returnType); if ((0, graphql_1.isLeafType)(namedType)) { return document; } const possibleTypes = (0, graphql_1.isAbstractType)(namedType) ? targetSchema.getPossibleTypes(namedType) : [namedType]; const rootTypeNames = (0, utils_1.getRootTypeNames)(targetSchema); const typeInfo = new graphql_1.TypeInfo(targetSchema); const visitorKeys = { Document: ['definitions'], OperationDefinition: ['selectionSet'], SelectionSet: ['selections'], InlineFragment: ['selectionSet'], FragmentDefinition: ['selectionSet'], }; return (0, graphql_1.visit)(document, (0, graphql_1.visitWithTypeInfo)(typeInfo, { [graphql_1.Kind.FRAGMENT_DEFINITION]: (node) => { const typeName = node.typeCondition.name.value; if (!rootTypeNames.has(typeName)) { return false; } }, [graphql_1.Kind.FIELD]: (node) => { const fieldType = typeInfo.getType(); if (fieldType) { const fieldNamedType = (0, graphql_1.getNamedType)(fieldType); if ((0, graphql_1.isAbstractType)(fieldNamedType) && fieldNamedType.name !== namedType.name && possibleTypes.length > 0) { return { ...node, selectionSet: { kind: graphql_1.Kind.SELECTION_SET, selections: possibleTypes.map(possibleType => ({ kind: graphql_1.Kind.INLINE_FRAGMENT, typeCondition: { kind: graphql_1.Kind.NAMED_TYPE, name: { kind: graphql_1.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); }