001// --------------------------------------------------------------------------------
002// Copyright 2002-2026 Echo Three, LLC
003//
004// Licensed under the Apache License, Version 2.0 (the "License");
005// you may not use this file except in compliance with the License.
006// You may obtain a copy of the License at
007//
008//     http://www.apache.org/licenses/LICENSE-2.0
009//
010// Unless required by applicable law or agreed to in writing, software
011// distributed under the License is distributed on an "AS IS" BASIS,
012// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013// See the License for the specific language governing permissions and
014// limitations under the License.
015// --------------------------------------------------------------------------------
016
017package com.echothree.control.user.graphql.server.strategy;
018
019import graphql.ExecutionResult;
020import graphql.annotations.strategies.EnhancedExecutionStrategy;
021import graphql.execution.ExecutionContext;
022import graphql.execution.ExecutionStrategyParameters;
023import graphql.execution.NonNullableFieldValidator;
024import graphql.schema.GraphQLObjectType;
025import java.util.Map;
026import java.util.concurrent.CompletableFuture;
027
028public class GraphQlExecutionStrategy
029        extends EnhancedExecutionStrategy {
030
031    private static final String CLIENT_MUTATION_ID = "clientMutationId";
032
033    @Override
034    protected CompletableFuture<ExecutionResult> resolveField(ExecutionContext executionContext,
035            ExecutionStrategyParameters parameters) {
036        CompletableFuture<ExecutionResult> result;
037        var parentInfo = parameters.getExecutionStepInfo();
038        var parentType = (GraphQLObjectType)parentInfo.getUnwrappedNonNullType();
039        var fieldDefinition = getFieldDef(executionContext.getGraphQLSchema(), parentType, parameters.getField().getSingleField());
040
041        if(fieldDefinition != null && CLIENT_MUTATION_ID.equals(fieldDefinition.getName())) {
042            // Use the current mutation's coerced arguments, not the first AST field.
043            // This handles variables, defaults, aliases, fragments, and omitted IDs.
044            var input = parentInfo.getArgument("input");
045            var clientMutationId = input instanceof Map<?, ?> map ? map.get(CLIENT_MUTATION_ID) : null;
046            var fieldInfo = createExecutionStepInfo(executionContext, parameters, fieldDefinition, parentType);
047            var valueParameters = parameters.transform(builder -> builder
048                    .executionStepInfo(fieldInfo)
049                    .nonNullFieldValidator(new NonNullableFieldValidator(executionContext, fieldInfo))
050                    .source(clientMutationId));
051
052            result = completeValue(executionContext, valueParameters).getFieldValue();
053        } else {
054            result = super.resolveField(executionContext, parameters);
055        }
056
057        return result;
058    }
059
060}