"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.finalizeGatewayRequest = void 0;
const graphql_1 = require("graphql");
const utils_1 = require("@graphql-tools/utils");
const getDocumentMetadata_js_1 = require("./getDocumentMetadata.js");
function finalizeGatewayDocument(targetSchema, fragments, operations) {
    let usedVariables = [];
    let usedFragments = [];
    const newOperations = [];
    let newFragments = [];
    const validFragments = [];
    const validFragmentsWithType = Object.create(null);
    for (const fragment of fragments) {
        const typeName = fragment.typeCondition.name.value;
        const type = targetSchema.getType(typeName);
        if (type != null) {
            validFragments.push(fragment);
            validFragmentsWithType[fragment.name.value] = type;
        }
    }
    let fragmentSet = Object.create(null);
    for (const operation of operations) {
        const type = (0, utils_1.getDefinedRootType)(targetSchema, operation.operation);
        const { selectionSet, usedFragments: operationUsedFragments, usedVariables: operationUsedVariables, } = finalizeSelectionSet(targetSchema, type, validFragmentsWithType, operation.selectionSet);
        usedFragments = union(usedFragments, operationUsedFragments);
        const { usedVariables: collectedUsedVariables, newFragments: collectedNewFragments, fragmentSet: collectedFragmentSet, } = collectFragmentVariables(targetSchema, fragmentSet, validFragments, validFragmentsWithType, usedFragments);
        const operationOrFragmentVariables = union(operationUsedVariables, collectedUsedVariables);
        usedVariables = union(usedVariables, operationOrFragmentVariables);
        newFragments = collectedNewFragments;
        fragmentSet = collectedFragmentSet;
        const variableDefinitions = (operation.variableDefinitions ?? []).filter((variable) => operationOrFragmentVariables.indexOf(variable.variable.name.value) !== -1);
        // Prevent unnecessary __typename in Subscription
        if (operation.operation === 'subscription') {
            selectionSet.selections = selectionSet.selections.filter((selection) => selection.kind !== graphql_1.Kind.FIELD || selection.name.value !== '__typename');
        }
        newOperations.push({
            kind: graphql_1.Kind.OPERATION_DEFINITION,
            operation: operation.operation,
            name: operation.name,
            directives: operation.directives,
            variableDefinitions,
            selectionSet,
        });
    }
    const newDocument = {
        kind: graphql_1.Kind.DOCUMENT,
        definitions: [...newOperations, ...newFragments],
    };
    return {
        usedVariables,
        newDocument,
    };
}
function finalizeGatewayRequest(originalRequest, delegationContext) {
    let { document, variables } = originalRequest;
    let { operations, fragments } = (0, getDocumentMetadata_js_1.getDocumentMetadata)(document);
    const { targetSchema, args } = delegationContext;
    if (args) {
        const requestWithNewVariables = addVariablesToRootFields(targetSchema, operations, args);
        operations = requestWithNewVariables.newOperations;
        variables = Object.assign({}, variables ?? {}, requestWithNewVariables.newVariables);
    }
    const { usedVariables, newDocument } = finalizeGatewayDocument(targetSchema, fragments, operations);
    const newVariables = {};
    if (variables != null) {
        for (const variableName of usedVariables) {
            const variableValue = variables[variableName];
            if (variableValue !== undefined) {
                newVariables[variableName] = variableValue;
            }
        }
    }
    return {
        ...originalRequest,
        document: newDocument,
        variables: newVariables,
    };
}
exports.finalizeGatewayRequest = finalizeGatewayRequest;
function addVariablesToRootFields(targetSchema, operations, args) {
    const newVariables = Object.create(null);
    const newOperations = operations.map((operation) => {
        const variableDefinitionMap = (operation.variableDefinitions ?? []).reduce((prev, def) => ({
            ...prev,
            [def.variable.name.value]: def,
        }), {});
        const type = (0, utils_1.getDefinedRootType)(targetSchema, operation.operation);
        const newSelections = [];
        for (const selection of operation.selectionSet.selections) {
            if (selection.kind === graphql_1.Kind.FIELD) {
                const argumentNodes = selection.arguments ?? [];
                const argumentNodeMap = argumentNodes.reduce((prev, argument) => ({
                    ...prev,
                    [argument.name.value]: argument,
                }), {});
                const targetField = type.getFields()[selection.name.value];
                // excludes __typename
                if (targetField != null) {
                    updateArguments(targetField, argumentNodeMap, variableDefinitionMap, newVariables, args);
                }
                newSelections.push({
                    ...selection,
                    arguments: Object.values(argumentNodeMap),
                });
            }
            else {
                newSelections.push(selection);
            }
        }
        const newSelectionSet = {
            kind: graphql_1.Kind.SELECTION_SET,
            selections: newSelections,
        };
        return {
            ...operation,
            variableDefinitions: Object.values(variableDefinitionMap),
            selectionSet: newSelectionSet,
        };
    });
    return {
        newOperations,
        newVariables,
    };
}
function updateArguments(targetField, argumentNodeMap, variableDefinitionMap, variableValues, newArgs) {
    const generateVariableName = (0, utils_1.createVariableNameGenerator)(variableDefinitionMap);
    for (const argument of targetField.args) {
        const argName = argument.name;
        const argType = argument.type;
        if (argName in newArgs) {
            (0, utils_1.updateArgument)(argumentNodeMap, variableDefinitionMap, variableValues, argName, generateVariableName(argName), argType, (0, utils_1.serializeInputValue)(argType, newArgs[argName]));
        }
    }
}
function collectFragmentVariables(targetSchema, fragmentSet, validFragments, validFragmentsWithType, usedFragments) {
    let remainingFragments = usedFragments.slice();
    let usedVariables = [];
    const newFragments = [];
    while (remainingFragments.length !== 0) {
        const nextFragmentName = remainingFragments.pop();
        const fragment = validFragments.find(fr => fr.name.value === nextFragmentName);
        if (fragment != null) {
            const name = nextFragmentName;
            const typeName = fragment.typeCondition.name.value;
            const type = targetSchema.getType(typeName);
            if (type == null) {
                throw new Error(`Fragment reference type "${typeName}", but the type is not contained within the target schema.`);
            }
            const { selectionSet, usedFragments: fragmentUsedFragments, usedVariables: fragmentUsedVariables, } = finalizeSelectionSet(targetSchema, type, validFragmentsWithType, fragment.selectionSet);
            remainingFragments = union(remainingFragments, fragmentUsedFragments);
            usedVariables = union(usedVariables, fragmentUsedVariables);
            if (name && !(name in fragmentSet)) {
                fragmentSet[name] = true;
                newFragments.push({
                    kind: graphql_1.Kind.FRAGMENT_DEFINITION,
                    name: {
                        kind: graphql_1.Kind.NAME,
                        value: name,
                    },
                    typeCondition: fragment.typeCondition,
                    selectionSet,
                });
            }
        }
    }
    return {
        usedVariables,
        newFragments,
        fragmentSet,
    };
}
const filteredSelectionSetVisitorKeys = {
    SelectionSet: ['selections'],
    Field: ['selectionSet'],
    InlineFragment: ['selectionSet'],
    FragmentDefinition: ['selectionSet'],
};
const variablesVisitorKeys = {
    SelectionSet: ['selections'],
    Field: ['arguments', 'directives', 'selectionSet'],
    Argument: ['value'],
    InlineFragment: ['directives', 'selectionSet'],
    FragmentSpread: ['directives'],
    FragmentDefinition: ['selectionSet'],
    ObjectValue: ['fields'],
    ObjectField: ['name', 'value'],
    Directive: ['arguments'],
    ListValue: ['values'],
};
function finalizeSelectionSet(schema, type, validFragments, selectionSet) {
    const usedFragments = [];
    const usedVariables = [];
    const typeInfo = graphql_1.versionInfo.major < 16
        ? new graphql_1.TypeInfo(schema, undefined, type)
        : new graphql_1.TypeInfo(schema, type);
    const filteredSelectionSet = (0, graphql_1.visit)(selectionSet, (0, graphql_1.visitWithTypeInfo)(typeInfo, {
        [graphql_1.Kind.FIELD]: {
            enter: node => {
                const parentType = typeInfo.getParentType();
                if ((0, graphql_1.isObjectType)(parentType) || (0, graphql_1.isInterfaceType)(parentType)) {
                    const fields = parentType.getFields();
                    const field = node.name.value === '__typename' ? graphql_1.TypeNameMetaFieldDef : fields[node.name.value];
                    if (!field) {
                        return null;
                    }
                    const args = field.args != null ? field.args : [];
                    const argsMap = Object.create(null);
                    for (const arg of args) {
                        argsMap[arg.name] = arg;
                    }
                    if (node.arguments != null) {
                        const newArgs = [];
                        for (const arg of node.arguments) {
                            if (arg.name.value in argsMap) {
                                newArgs.push(arg);
                            }
                        }
                        if (newArgs.length !== node.arguments.length) {
                            return {
                                ...node,
                                arguments: newArgs,
                            };
                        }
                    }
                }
            },
            leave: node => {
                const type = typeInfo.getType();
                if (type == null) {
                    throw new Error(`No type was found for field node ${(0, utils_1.inspect)(node)}.`);
                }
                const namedType = (0, graphql_1.getNamedType)(type);
                if (!schema.getType(namedType.name) == null) {
                    return null;
                }
                if ((0, graphql_1.isObjectType)(namedType) || (0, graphql_1.isInterfaceType)(namedType)) {
                    const selections = node.selectionSet != null ? node.selectionSet.selections : null;
                    if (selections == null || selections.length === 0) {
                        return null;
                    }
                }
            },
        },
        [graphql_1.Kind.FRAGMENT_SPREAD]: {
            enter: node => {
                if (!(node.name.value in validFragments)) {
                    return null;
                }
                const parentType = typeInfo.getParentType();
                const innerType = validFragments[node.name.value];
                if (!(0, utils_1.implementsAbstractType)(schema, parentType, innerType)) {
                    return null;
                }
                usedFragments.push(node.name.value);
            },
        },
        [graphql_1.Kind.INLINE_FRAGMENT]: {
            enter: node => {
                if (node.typeCondition != null) {
                    const parentType = typeInfo.getParentType();
                    const innerType = schema.getType(node.typeCondition.name.value);
                    if (!(0, utils_1.implementsAbstractType)(schema, parentType, innerType)) {
                        return null;
                    }
                }
            },
        },
        [graphql_1.Kind.SELECTION_SET]: {
            leave: node => {
                const parentType = typeInfo.getParentType();
                if (parentType != null && (0, graphql_1.isAbstractType)(parentType)) {
                    const selections = node.selections.concat([
                        {
                            kind: graphql_1.Kind.FIELD,
                            name: {
                                kind: graphql_1.Kind.NAME,
                                value: '__typename',
                            },
                        },
                    ]);
                    return {
                        ...node,
                        selections,
                    };
                }
            },
        },
    }), 
    // 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
    filteredSelectionSetVisitorKeys);
    (0, graphql_1.visit)(filteredSelectionSet, {
        [graphql_1.Kind.VARIABLE]: variableNode => {
            usedVariables.push(variableNode.name.value);
        },
    }, 
    // 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
    variablesVisitorKeys);
    return {
        selectionSet: filteredSelectionSet,
        usedFragments,
        usedVariables,
    };
}
function union(...arrays) {
    const cache = Object.create(null);
    const result = [];
    for (const array of arrays) {
        for (const item of array) {
            if (!(item in cache)) {
                cache[item] = true;
                result.push(item);
            }
        }
    }
    return result;
}
