"use strict";;
const {
    __DEV__
} = require("@apollo/client/utilities/environment");

Object.defineProperty(exports, "__esModule", { value: true });
exports.addTypenameToDocument = void 0;
exports.removeDirectivesFromDocument = removeDirectivesFromDocument;
exports.removeConnectionDirectiveFromDocument = removeConnectionDirectiveFromDocument;
exports.removeArgumentsFromDocument = removeArgumentsFromDocument;
exports.removeFragmentSpreadFromDocument = removeFragmentSpreadFromDocument;
exports.buildQueryFromSelectionSet = buildQueryFromSelectionSet;
exports.removeClientSetsFromDocument = removeClientSetsFromDocument;
exports.addNonReactiveToNamedFragments = addNonReactiveToNamedFragments;
const graphql_1 = require("graphql");
const invariant_1 = require("@apollo/client/utilities/invariant");
const arrays_js_1 = require("../common/arrays.cjs");
const fragments_js_1 = require("./fragments.cjs");
const getFromAST_js_1 = require("./getFromAST.cjs");
const storeUtils_js_1 = require("./storeUtils.cjs");
const TYPENAME_FIELD = {
    kind: graphql_1.Kind.FIELD,
    name: {
        kind: graphql_1.Kind.NAME,
        value: "__typename",
    },
};
function isEmpty(op, fragmentMap) {
    return (!op ||
        op.selectionSet.selections.every((selection) => selection.kind === graphql_1.Kind.FRAGMENT_SPREAD &&
            isEmpty(fragmentMap[selection.name.value], fragmentMap)));
}
function nullIfDocIsEmpty(doc) {
    return (isEmpty((0, getFromAST_js_1.getOperationDefinition)(doc) || (0, getFromAST_js_1.getFragmentDefinition)(doc), (0, fragments_js_1.createFragmentMap)((0, getFromAST_js_1.getFragmentDefinitions)(doc)))) ?
        null
        : doc;
}
function getDirectiveMatcher(configs) {
    const names = new Map();
    const tests = new Map();
    configs.forEach((directive) => {
        if (directive) {
            if (directive.name) {
                names.set(directive.name, directive);
            }
            else if (directive.test) {
                tests.set(directive.test, directive);
            }
        }
    });
    return (directive) => {
        let config = names.get(directive.name.value);
        if (!config && tests.size) {
            tests.forEach((testConfig, test) => {
                if (test(directive)) {
                    config = testConfig;
                }
            });
        }
        return config;
    };
}
function makeInUseGetterFunction(defaultKey) {
    const map = new Map();
    return function inUseGetterFunction(key = defaultKey) {
        let inUse = map.get(key);
        if (!inUse) {
            map.set(key, (inUse = {
                // Variable and fragment spread names used directly within this
                // operation or fragment definition, as identified by key. These sets
                // will be populated during the first traversal of the document in
                // removeDirectivesFromDocument below.
                variables: new Set(),
                fragmentSpreads: new Set(),
            }));
        }
        return inUse;
    };
}
function removeDirectivesFromDocument(directives, doc) {
    (0, getFromAST_js_1.checkDocument)(doc);
    // Passing empty strings to makeInUseGetterFunction means we handle anonymous
    // operations as if their names were "". Anonymous fragment definitions are
    // not supposed to be possible, but the same default naming strategy seems
    // appropriate for that case as well.
    const getInUseByOperationName = makeInUseGetterFunction("");
    const getInUseByFragmentName = makeInUseGetterFunction("");
    const getInUse = (ancestors) => {
        for (let p = 0, ancestor; p < ancestors.length && (ancestor = ancestors[p]); ++p) {
            if ((0, arrays_js_1.isArray)(ancestor))
                continue;
            if (ancestor.kind === graphql_1.Kind.OPERATION_DEFINITION) {
                // If an operation is anonymous, we use the empty string as its key.
                return getInUseByOperationName(ancestor.name && ancestor.name.value);
            }
            if (ancestor.kind === graphql_1.Kind.FRAGMENT_DEFINITION) {
                return getInUseByFragmentName(ancestor.name.value);
            }
        }
        __DEV__ && invariant_1.invariant.error(22);
        return null;
    };
    let operationCount = 0;
    for (let i = doc.definitions.length - 1; i >= 0; --i) {
        if (doc.definitions[i].kind === graphql_1.Kind.OPERATION_DEFINITION) {
            ++operationCount;
        }
    }
    const directiveMatcher = getDirectiveMatcher(directives);
    const shouldRemoveField = (nodeDirectives) => (0, arrays_js_1.isNonEmptyArray)(nodeDirectives) &&
        nodeDirectives
            .map(directiveMatcher)
            .some((config) => config && config.remove);
    const originalFragmentDefsByPath = new Map();
    // Any time the first traversal of the document below makes a change like
    // removing a fragment (by returning null), this variable should be set to
    // true. Once it becomes true, it should never be set to false again. If this
    // variable remains false throughout the traversal, then we can return the
    // original doc immediately without any modifications.
    let firstVisitMadeChanges = false;
    const fieldOrInlineFragmentVisitor = {
        enter(node) {
            if (shouldRemoveField(node.directives)) {
                firstVisitMadeChanges = true;
                return null;
            }
        },
    };
    const docWithoutDirectiveSubtrees = (0, graphql_1.visit)(doc, {
        // These two AST node types share the same implementation, defined above.
        Field: fieldOrInlineFragmentVisitor,
        InlineFragment: fieldOrInlineFragmentVisitor,
        VariableDefinition: {
            enter() {
                // VariableDefinition nodes do not count as variables in use, though
                // they do contain Variable nodes that might be visited below. To avoid
                // counting variable declarations as usages, we skip visiting the
                // contents of this VariableDefinition node by returning false.
                return false;
            },
        },
        Variable: {
            enter(node, _key, _parent, _path, ancestors) {
                const inUse = getInUse(ancestors);
                if (inUse) {
                    inUse.variables.add(node.name.value);
                }
            },
        },
        FragmentSpread: {
            enter(node, _key, _parent, _path, ancestors) {
                if (shouldRemoveField(node.directives)) {
                    firstVisitMadeChanges = true;
                    return null;
                }
                const inUse = getInUse(ancestors);
                if (inUse) {
                    inUse.fragmentSpreads.add(node.name.value);
                }
                // We might like to remove this FragmentSpread by returning null here if
                // the corresponding FragmentDefinition node is also going to be removed
                // by the logic below, but we can't control the relative order of those
                // events, so we have to postpone the removal of dangling FragmentSpread
                // nodes until after the current visit of the document has finished.
            },
        },
        FragmentDefinition: {
            enter(node, _key, _parent, path) {
                originalFragmentDefsByPath.set(JSON.stringify(path), node);
            },
            leave(node, _key, _parent, path) {
                const originalNode = originalFragmentDefsByPath.get(JSON.stringify(path));
                if (node === originalNode) {
                    // If the FragmentNode received by this leave function is identical to
                    // the one received by the corresponding enter function (above), then
                    // the visitor must not have made any changes within this
                    // FragmentDefinition node. This fragment definition may still be
                    // removed if there are no ...spread references to it, but it won't be
                    // removed just because it has only a __typename field.
                    return node;
                }
                if (
                // This logic applies only if the document contains one or more
                // operations, since removing all fragments from a document containing
                // only fragments makes the document useless.
                operationCount > 0 &&
                    node.selectionSet.selections.every((selection) => selection.kind === graphql_1.Kind.FIELD &&
                        selection.name.value === "__typename")) {
                    // This is a somewhat opinionated choice: if a FragmentDefinition ends
                    // up having no fields other than __typename, we remove the whole
                    // fragment definition, and later prune ...spread references to it.
                    getInUseByFragmentName(node.name.value).removed = true;
                    firstVisitMadeChanges = true;
                    return null;
                }
            },
        },
        Directive: {
            leave(node) {
                // If a matching directive is found, remove the directive itself. Note
                // that this does not remove the target (field, argument, etc) of the
                // directive, but only the directive itself.
                if (directiveMatcher(node)) {
                    firstVisitMadeChanges = true;
                    return null;
                }
            },
        },
    });
    if (!firstVisitMadeChanges) {
        // If our first pass did not change anything about the document, then there
        // is no cleanup we need to do, and we can return the original doc.
        return doc;
    }
    // Utility for making sure inUse.transitiveVars is recursively populated.
    // Because this logic assumes inUse.fragmentSpreads has been completely
    // populated and inUse.removed has been set if appropriate,
    // populateTransitiveVars must be called after that information has been
    // collected by the first traversal of the document.
    const populateTransitiveVars = (inUse) => {
        if (!inUse.transitiveVars) {
            inUse.transitiveVars = new Set(inUse.variables);
            if (!inUse.removed) {
                inUse.fragmentSpreads.forEach((childFragmentName) => {
                    populateTransitiveVars(getInUseByFragmentName(childFragmentName)).transitiveVars.forEach((varName) => {
                        inUse.transitiveVars.add(varName);
                    });
                });
            }
        }
        return inUse;
    };
    // Since we've been keeping track of fragment spreads used by particular
    // operations and fragment definitions, we now need to compute the set of all
    // spreads used (transitively) by any operations in the document.
    const allFragmentNamesUsed = new Set();
    docWithoutDirectiveSubtrees.definitions.forEach((def) => {
        if (def.kind === graphql_1.Kind.OPERATION_DEFINITION) {
            populateTransitiveVars(getInUseByOperationName(def.name && def.name.value)).fragmentSpreads.forEach((childFragmentName) => {
                allFragmentNamesUsed.add(childFragmentName);
            });
        }
        else if (def.kind === graphql_1.Kind.FRAGMENT_DEFINITION &&
            // If there are no operations in the document, then all fragment
            // definitions count as usages of their own fragment names. This heuristic
            // prevents accidentally removing all fragment definitions from the
            // document just because it contains no operations that use the fragments.
            operationCount === 0 &&
            !getInUseByFragmentName(def.name.value).removed) {
            allFragmentNamesUsed.add(def.name.value);
        }
    });
    // Now that we have added all fragment spreads used by operations to the
    // allFragmentNamesUsed set, we can complete the set by transitively adding
    // all fragment spreads used by those fragments, and so on.
    allFragmentNamesUsed.forEach((fragmentName) => {
        // Once all the childFragmentName strings added here have been seen already,
        // the top-level allFragmentNamesUsed.forEach loop will terminate.
        populateTransitiveVars(getInUseByFragmentName(fragmentName)).fragmentSpreads.forEach((childFragmentName) => {
            allFragmentNamesUsed.add(childFragmentName);
        });
    });
    const fragmentWillBeRemoved = (fragmentName) => !!(
    // A fragment definition will be removed if there are no spreads that refer
    // to it, or the fragment was explicitly removed because it had no fields
    // other than __typename.
    (!allFragmentNamesUsed.has(fragmentName) ||
        getInUseByFragmentName(fragmentName).removed));
    const enterVisitor = {
        enter(node) {
            if (fragmentWillBeRemoved(node.name.value)) {
                return null;
            }
        },
    };
    return nullIfDocIsEmpty((0, graphql_1.visit)(docWithoutDirectiveSubtrees, {
        // If the fragment is going to be removed, then leaving any dangling
        // FragmentSpread nodes with the same name would be a mistake.
        FragmentSpread: enterVisitor,
        // This is where the fragment definition is actually removed.
        FragmentDefinition: enterVisitor,
        OperationDefinition: {
            leave(node) {
                // Upon leaving each operation in the depth-first AST traversal, prune
                // any variables that are declared by the operation but unused within.
                if (node.variableDefinitions) {
                    const usedVariableNames = populateTransitiveVars(
                    // If an operation is anonymous, we use the empty string as its key.
                    getInUseByOperationName(node.name && node.name.value)).transitiveVars;
                    // According to the GraphQL spec, all variables declared by an
                    // operation must either be used by that operation or used by some
                    // fragment included transitively into that operation:
                    // https://spec.graphql.org/draft/#sec-All-Variables-Used
                    //
                    // To stay on the right side of this validation rule, if/when we
                    // remove the last $var references from an operation or its fragments,
                    // we must also remove the corresponding $var declaration from the
                    // enclosing operation. This pruning applies only to operations and
                    // not fragment definitions, at the moment. Fragments may be able to
                    // declare variables eventually, but today they can only consume them.
                    if (usedVariableNames.size < node.variableDefinitions.length) {
                        return {
                            ...node,
                            variableDefinitions: node.variableDefinitions.filter((varDef) => usedVariableNames.has(varDef.variable.name.value)),
                        };
                    }
                }
            },
        },
    }));
}
exports.addTypenameToDocument = Object.assign(function (doc) {
    return (0, graphql_1.visit)(doc, {
        SelectionSet: {
            enter(node, _key, parent) {
                // Don't add __typename to OperationDefinitions.
                if (parent &&
                    parent.kind ===
                        graphql_1.Kind.OPERATION_DEFINITION) {
                    return;
                }
                // No changes if no selections.
                const { selections } = node;
                if (!selections) {
                    return;
                }
                // If selections already have a __typename, or are part of an
                // introspection query, do nothing.
                const skip = selections.some((selection) => {
                    return ((0, storeUtils_js_1.isField)(selection) &&
                        (selection.name.value === "__typename" ||
                            selection.name.value.lastIndexOf("__", 0) === 0));
                });
                if (skip) {
                    return;
                }
                // If this SelectionSet is @export-ed as an input variable, it should
                // not have a __typename field (see issue #4691).
                const field = parent;
                if ((0, storeUtils_js_1.isField)(field) &&
                    field.directives &&
                    field.directives.some((d) => d.name.value === "export")) {
                    return;
                }
                // Create and return a new SelectionSet with a __typename Field.
                return {
                    ...node,
                    selections: [...selections, TYPENAME_FIELD],
                };
            },
        },
    });
}, {
    added(field) {
        return field === TYPENAME_FIELD;
    },
});
const connectionRemoveConfig = {
    test: (directive) => {
        const willRemove = directive.name.value === "connection";
        if (willRemove) {
            if (!directive.arguments ||
                !directive.arguments.some((arg) => arg.name.value === "key")) {
                __DEV__ && invariant_1.invariant.warn(23);
            }
        }
        return willRemove;
    },
};
function removeConnectionDirectiveFromDocument(doc) {
    return removeDirectivesFromDocument([connectionRemoveConfig], (0, getFromAST_js_1.checkDocument)(doc));
}
function hasDirectivesInSelectionSet(directives, selectionSet, nestedCheck = true) {
    return (!!selectionSet &&
        selectionSet.selections &&
        selectionSet.selections.some((selection) => hasDirectivesInSelection(directives, selection, nestedCheck)));
}
function hasDirectivesInSelection(directives, selection, nestedCheck = true) {
    if (!(0, storeUtils_js_1.isField)(selection)) {
        return true;
    }
    if (!selection.directives) {
        return false;
    }
    return (selection.directives.some(getDirectiveMatcher(directives)) ||
        (nestedCheck &&
            hasDirectivesInSelectionSet(directives, selection.selectionSet, nestedCheck)));
}
function getArgumentMatcher(config) {
    return function argumentMatcher(argument) {
        return config.some((aConfig) => argument.value &&
            argument.value.kind === graphql_1.Kind.VARIABLE &&
            argument.value.name &&
            (aConfig.name === argument.value.name.value ||
                (aConfig.test && aConfig.test(argument))));
    };
}
function removeArgumentsFromDocument(config, doc) {
    const argMatcher = getArgumentMatcher(config);
    return nullIfDocIsEmpty((0, graphql_1.visit)(doc, {
        OperationDefinition: {
            enter(node) {
                return {
                    ...node,
                    // Remove matching top level variables definitions.
                    variableDefinitions: node.variableDefinitions ?
                        node.variableDefinitions.filter((varDef) => !config.some((arg) => arg.name === varDef.variable.name.value))
                        : [],
                };
            },
        },
        Field: {
            enter(node) {
                // If `remove` is set to true for an argument, and an argument match
                // is found for a field, remove the field as well.
                const shouldRemoveField = config.some((argConfig) => argConfig.remove);
                if (shouldRemoveField) {
                    let argMatchCount = 0;
                    if (node.arguments) {
                        node.arguments.forEach((arg) => {
                            if (argMatcher(arg)) {
                                argMatchCount += 1;
                            }
                        });
                    }
                    if (argMatchCount === 1) {
                        return null;
                    }
                }
            },
        },
        Argument: {
            enter(node) {
                // Remove all matching arguments.
                if (argMatcher(node)) {
                    return null;
                }
            },
        },
    }));
}
function removeFragmentSpreadFromDocument(config, doc) {
    function enter(node) {
        if (config.some((def) => def.name === node.name.value)) {
            return null;
        }
    }
    return nullIfDocIsEmpty((0, graphql_1.visit)(doc, {
        FragmentSpread: { enter },
        FragmentDefinition: { enter },
    }));
}
// If the incoming document is a query, return it as is. Otherwise, build a
// new document containing a query operation based on the selection set
// of the previous main operation.
function buildQueryFromSelectionSet(document) {
    const definition = (0, getFromAST_js_1.getMainDefinition)(document);
    const definitionOperation = definition.operation;
    if (definitionOperation === "query") {
        // Already a query, so return the existing document.
        return document;
    }
    // Build a new query using the selection set of the main operation.
    const modifiedDoc = (0, graphql_1.visit)(document, {
        OperationDefinition: {
            enter(node) {
                return {
                    ...node,
                    operation: "query",
                };
            },
        },
    });
    return modifiedDoc;
}
// Remove fields / selection sets that include an @client directive.
function removeClientSetsFromDocument(document) {
    (0, getFromAST_js_1.checkDocument)(document);
    let modifiedDoc = removeDirectivesFromDocument([
        {
            test: (directive) => directive.name.value === "client",
            remove: true,
        },
    ], document);
    return modifiedDoc;
}
function addNonReactiveToNamedFragments(document) {
    (0, getFromAST_js_1.checkDocument)(document);
    return (0, graphql_1.visit)(document, {
        FragmentSpread: (node) => {
            // Do not add `@nonreactive` if the fragment is marked with `@unmask`
            // since we want to react to changes in this fragment.
            if (node.directives?.some((directive) => directive.name.value === "unmask")) {
                return;
            }
            return {
                ...node,
                directives: [
                    ...(node.directives || []),
                    {
                        kind: graphql_1.Kind.DIRECTIVE,
                        name: { kind: graphql_1.Kind.NAME, value: "nonreactive" },
                    },
                ],
            };
        },
    });
}
//# sourceMappingURL=transform.cjs.map
