"use strict";
// adapted from https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby-source-graphql/src/batching/merge-queries.js
Object.defineProperty(exports, "__esModule", { value: true });
exports.mergeRequests = void 0;
const graphql_1 = require("graphql");
const utils_1 = require("@graphql-tools/utils");
const prefix_js_1 = require("./prefix.js");
/**
 * Merge multiple queries into a single query in such a way that query results
 * can be split and transformed as if they were obtained by running original queries.
 *
 * Merging algorithm involves several transformations:
 *  1. Replace top-level fragment spreads with inline fragments (... on Query {})
 *  2. Add unique aliases to all top-level query fields (including those on inline fragments)
 *  3. Prefix all variable definitions and variable usages
 *  4. Prefix names (and spreads) of fragments
 *
 * i.e transform:
 *   [
 *     `query Foo($id: ID!) { foo, bar(id: $id), ...FooQuery }
 *     fragment FooQuery on Query { baz }`,
 *
 *    `query Bar($id: ID!) { foo: baz, bar(id: $id), ... on Query { baz } }`
 *   ]
 * to:
 *   query (
 *     $graphqlTools1_id: ID!
 *     $graphqlTools2_id: ID!
 *   ) {
 *     graphqlTools1_foo: foo,
 *     graphqlTools1_bar: bar(id: $graphqlTools1_id)
 *     ... on Query {
 *       graphqlTools1__baz: baz
 *     }
 *     graphqlTools1__foo: baz
 *     graphqlTools1__bar: bar(id: $graphqlTools1__id)
 *     ... on Query {
 *       graphqlTools1__baz: baz
 *     }
 *   }
 */
function mergeRequests(requests, extensionsReducer) {
    const mergedVariables = Object.create(null);
    const mergedVariableDefinitions = [];
    const mergedSelections = [];
    const mergedFragmentDefinitions = [];
    let mergedExtensions = Object.create(null);
    for (let index = 0; index < requests.length; index++) {
        const request = requests[index];
        const prefixedRequests = prefixRequest((0, prefix_js_1.createPrefix)(index), request);
        for (const def of prefixedRequests.document.definitions) {
            if (isOperationDefinition(def)) {
                mergedSelections.push(...def.selectionSet.selections);
                if (def.variableDefinitions) {
                    mergedVariableDefinitions.push(...def.variableDefinitions);
                }
            }
            if (isFragmentDefinition(def)) {
                mergedFragmentDefinitions.push(def);
            }
        }
        Object.assign(mergedVariables, prefixedRequests.variables);
        mergedExtensions = extensionsReducer(mergedExtensions, request);
    }
    const firstRequest = requests[0];
    const operationType = firstRequest.operationType ?? (0, utils_1.getOperationASTFromRequest)(firstRequest).operation;
    const mergedOperationDefinition = {
        kind: graphql_1.Kind.OPERATION_DEFINITION,
        operation: operationType,
        variableDefinitions: mergedVariableDefinitions,
        selectionSet: {
            kind: graphql_1.Kind.SELECTION_SET,
            selections: mergedSelections,
        },
    };
    const operationName = firstRequest.operationName ?? firstRequest.info?.operation?.name?.value;
    if (operationName) {
        mergedOperationDefinition.name = {
            kind: graphql_1.Kind.NAME,
            value: operationName,
        };
    }
    return {
        document: {
            kind: graphql_1.Kind.DOCUMENT,
            definitions: [mergedOperationDefinition, ...mergedFragmentDefinitions],
        },
        variables: mergedVariables,
        extensions: mergedExtensions,
        context: requests[0].context,
        info: requests[0].info,
        operationType,
        rootValue: requests[0].rootValue,
    };
}
exports.mergeRequests = mergeRequests;
function prefixRequest(prefix, request) {
    const executionVariables = request.variables ?? {};
    function prefixNode(node) {
        return prefixNodeName(node, prefix);
    }
    let prefixedDocument = aliasTopLevelFields(prefix, request.document);
    const executionVariableNames = Object.keys(executionVariables);
    const hasFragmentDefinitions = request.document.definitions.some(def => isFragmentDefinition(def));
    const fragmentSpreadImpl = {};
    if (executionVariableNames.length > 0 || hasFragmentDefinitions) {
        prefixedDocument = (0, graphql_1.visit)(prefixedDocument, {
            [graphql_1.Kind.VARIABLE]: prefixNode,
            [graphql_1.Kind.FRAGMENT_DEFINITION]: prefixNode,
            [graphql_1.Kind.FRAGMENT_SPREAD]: node => {
                node = prefixNodeName(node, prefix);
                fragmentSpreadImpl[node.name.value] = true;
                return node;
            },
        });
    }
    const prefixedVariables = {};
    for (const variableName of executionVariableNames) {
        prefixedVariables[prefix + variableName] = executionVariables[variableName];
    }
    if (hasFragmentDefinitions) {
        prefixedDocument = {
            ...prefixedDocument,
            definitions: prefixedDocument.definitions.filter(def => {
                return !isFragmentDefinition(def) || fragmentSpreadImpl[def.name.value];
            }),
        };
    }
    return {
        document: prefixedDocument,
        variables: prefixedVariables,
    };
}
/**
 * Adds prefixed aliases to top-level fields of the query.
 *
 * @see aliasFieldsInSelection for implementation details
 */
function aliasTopLevelFields(prefix, document) {
    const transformer = {
        [graphql_1.Kind.OPERATION_DEFINITION]: (def) => {
            const { selections } = def.selectionSet;
            return {
                ...def,
                selectionSet: {
                    ...def.selectionSet,
                    selections: aliasFieldsInSelection(prefix, selections, document),
                },
            };
        },
    };
    return (0, graphql_1.visit)(document, transformer, {
        [graphql_1.Kind.DOCUMENT]: [`definitions`],
    });
}
/**
 * Add aliases to fields of the selection, including top-level fields of inline fragments.
 * Fragment spreads are converted to inline fragments and their top-level fields are also aliased.
 *
 * Note that this method is shallow. It adds aliases only to the top-level fields and doesn't
 * descend to field sub-selections.
 *
 * For example, transforms:
 *   {
 *     foo
 *     ... on Query { foo }
 *     ...FragmentWithBarField
 *   }
 * To:
 *   {
 *     graphqlTools1_foo: foo
 *     ... on Query { graphqlTools1_foo: foo }
 *     ... on Query { graphqlTools1_bar: bar }
 *   }
 */
function aliasFieldsInSelection(prefix, selections, document) {
    return selections.map(selection => {
        switch (selection.kind) {
            case graphql_1.Kind.INLINE_FRAGMENT:
                return aliasFieldsInInlineFragment(prefix, selection, document);
            case graphql_1.Kind.FRAGMENT_SPREAD: {
                const inlineFragment = inlineFragmentSpread(selection, document);
                return aliasFieldsInInlineFragment(prefix, inlineFragment, document);
            }
            case graphql_1.Kind.FIELD:
            default:
                return aliasField(selection, prefix);
        }
    });
}
/**
 * Add aliases to top-level fields of the inline fragment.
 * Returns new inline fragment node.
 *
 * For Example, transforms:
 *   ... on Query { foo, ... on Query { bar: foo } }
 * To
 *   ... on Query { graphqlTools1_foo: foo, ... on Query { graphqlTools1_bar: foo } }
 */
function aliasFieldsInInlineFragment(prefix, fragment, document) {
    const { selections } = fragment.selectionSet;
    return {
        ...fragment,
        selectionSet: {
            ...fragment.selectionSet,
            selections: aliasFieldsInSelection(prefix, selections, document),
        },
    };
}
/**
 * Replaces fragment spread with inline fragment
 *
 * Example:
 *   query { ...Spread }
 *   fragment Spread on Query { bar }
 *
 * Transforms to:
 *   query { ... on Query { bar } }
 */
function inlineFragmentSpread(spread, document) {
    const fragment = document.definitions.find(def => isFragmentDefinition(def) && def.name.value === spread.name.value);
    if (!fragment) {
        throw new Error(`Fragment ${spread.name.value} does not exist`);
    }
    const { typeCondition, selectionSet } = fragment;
    return {
        kind: graphql_1.Kind.INLINE_FRAGMENT,
        typeCondition,
        selectionSet,
        directives: spread.directives,
    };
}
function prefixNodeName(namedNode, prefix) {
    return {
        ...namedNode,
        name: {
            ...namedNode.name,
            value: prefix + namedNode.name.value,
        },
    };
}
/**
 * Returns a new FieldNode with prefixed alias
 *
 * Example. Given prefix === "graphqlTools1_" transforms:
 *   { foo } -> { graphqlTools1_foo: foo }
 *   { foo: bar } -> { graphqlTools1_foo: bar }
 */
function aliasField(field, aliasPrefix) {
    const aliasNode = field.alias ? field.alias : field.name;
    return {
        ...field,
        alias: {
            ...aliasNode,
            value: aliasPrefix + aliasNode.value,
        },
    };
}
function isOperationDefinition(def) {
    return def.kind === graphql_1.Kind.OPERATION_DEFINITION;
}
function isFragmentDefinition(def) {
    return def.kind === graphql_1.Kind.FRAGMENT_DEFINITION;
}
