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.command;
018
019import com.echothree.control.user.graphql.common.form.ExecuteGraphQlForm;
020import com.echothree.control.user.graphql.common.result.GraphQlResultFactory;
021import com.echothree.control.user.graphql.server.cache.GraphQlDocumentCache;
022import com.echothree.control.user.graphql.server.schema.util.GraphQlSchemaUtils;
023import com.echothree.model.control.graphql.server.util.BaseGraphQl;
024import com.echothree.model.control.graphql.server.util.GraphQlExecutionContext;
025import com.echothree.util.common.command.BaseResult;
026import com.echothree.util.common.message.ExecutionErrors;
027import com.echothree.util.common.string.GraphQlUtils;
028import com.echothree.util.common.validation.FieldDefinition;
029import com.echothree.util.common.validation.FieldType;
030import com.echothree.util.server.control.BaseSimpleCommand;
031import com.google.gson.JsonParseException;
032import graphql.ExecutionInput;
033import graphql.ExecutionResult;
034import graphql.GraphQL;
035import graphql.GraphQLException;
036import graphql.annotations.strategies.EnhancedExecutionStrategy;
037import java.util.LinkedHashMap;
038import java.util.List;
039import java.util.Map;
040import javax.enterprise.context.Dependent;
041
042@Dependent
043public class ExecuteGraphQlCommand
044        extends BaseSimpleCommand<ExecuteGraphQlForm> {
045    
046    private final static List<FieldDefinition> FORM_FIELD_DEFINITIONS;
047    
048    static {
049        FORM_FIELD_DEFINITIONS = List.of(
050                new FieldDefinition("ReadOnly", FieldType.BOOLEAN, true, null, null),
051                new FieldDefinition("Query", FieldType.STRING, false, 1L, null),
052                new FieldDefinition("Variables", FieldType.STRING, false, 1L, null),
053                new FieldDefinition("OperationName", FieldType.STRING, false, 1L, null),
054                new FieldDefinition("Json", FieldType.STRING, false, 1L, null),
055                // RemoteInet4Address is purposefully validated only as a string, as it's passed
056                // to other UCs that will validate it as an IPv4 address and format as necessary
057                // for use.
058                new FieldDefinition("RemoteInet4Address", FieldType.STRING, false, 1L, null)
059        );
060    }
061    
062    /** Creates a new instance of ExecuteGraphQlCommand */
063    public ExecuteGraphQlCommand() {
064        super(null, FORM_FIELD_DEFINITIONS, false);
065    }
066    
067    private static final String GRAPHQL_QUERY = "query";
068    private static final String GRAPHQL_OPERATION_NAME = "operationName";
069    private static final String GRAPHQL_VARIABLES = "variables";
070    
071    public String toJson(ExecutionResult executionResult)  {
072        // Contents of the GraphQL Response are specified here:
073        // http://graphql.org/learn/serving-over-http/
074        var executionResultMap = new LinkedHashMap<String, Object>();
075        
076        if(!executionResult.getErrors().isEmpty()) {
077            executionResultMap.put("errors", executionResult.getErrors());
078        }
079        executionResultMap.put("data", executionResult.getData());
080        
081        return GraphQlUtils.getInstance().toJson(executionResultMap);
082    }
083    
084    @Override
085    protected BaseResult execute() {
086        var result = GraphQlResultFactory.getExecuteGraphQlResult();
087
088        try {
089            var readOnly = Boolean.parseBoolean(form.getReadOnly());
090            var query = form.getQuery();
091            var variables = form.getVariables();
092            var operationName = form.getOperationName();
093            var json = form.getJson();
094
095            var graphQL = GraphQL
096                    .newGraphQL(readOnly? GraphQlSchemaUtils.getInstance().getReadOnlySchema() : GraphQlSchemaUtils.getInstance().getSchema())
097                    .queryExecutionStrategy(new EnhancedExecutionStrategy())
098                    .preparsedDocumentProvider(GraphQlDocumentCache.getInstance())
099                    .build();
100
101            Map<String, Object> parsedVariables = null;
102            if(variables != null) {
103                Object possibleVariables = GraphQlUtils.getInstance().toMap(variables);
104
105                if(possibleVariables instanceof Map) {
106                    parsedVariables = (Map<String, Object>)possibleVariables;
107                } else {
108                    log.error("Discarding parsedVariables, not an instance of Map");
109                }
110            }
111
112            if(json != null) {
113                var body = GraphQlUtils.getInstance().toMap(json);
114                var possibleQuery = body.get(GRAPHQL_QUERY);
115                var possibleOperationName = body.get(GRAPHQL_OPERATION_NAME);
116                var possibleVariables = body.get(GRAPHQL_VARIABLES);
117
118                // Query form field takes priority of Json's query.
119                if(possibleQuery != null && query == null) {
120                    if(possibleQuery instanceof String string) {
121                        query = string;
122                    } else {
123                        log.error("Discarding query, not an instance of String");
124                    }
125                }
126
127                // OperationName form field takes priority of Json's operationName.
128                if(possibleOperationName != null && operationName == null) {
129                    if(possibleOperationName instanceof String string) {
130                        operationName = string;
131                    } else {
132                        log.error("Discarding operationName, not an instance of String");
133                    }
134                }
135
136                // Variables form field takes priority of Json's variables.
137                if(possibleVariables != null && variables == null) {
138                    if(possibleVariables instanceof Map) {
139                        parsedVariables = (Map<String, Object>)possibleVariables;
140                    } else {
141                        log.error("Discarding parsedVariables, not an instance of Map");
142                    }
143                }
144            }
145            
146            // query MUST be present.
147            if(query != null) {
148                var graphQlExecutionContext = new GraphQlExecutionContext(getUserVisitPK(), getUserVisit(),
149                        getUserSession(), form.getRemoteInet4Address());
150                var builder = ExecutionInput.newExecutionInput()
151                        .query(query)
152                        .operationName(operationName)
153                        .graphQLContext(Map.of(
154                                BaseGraphQl.GRAPHQL_EXECUTION_CONTEXT, graphQlExecutionContext))
155                        .root(new Object());
156                
157                if(parsedVariables != null) {
158                    builder.variables(parsedVariables);
159                }
160
161                var executionResult = graphQL.execute(builder.build());
162                result.setExecutionResult(toJson(executionResult));
163            } else {
164                addExecutionError(ExecutionErrors.InvalidParameterCount.name());
165            }
166        } catch (JsonParseException jpe) {
167            addExecutionError(ExecutionErrors.JsonParseError.name(), jpe.getMessage());
168        } catch (GraphQLException gqle) {
169            addExecutionError(ExecutionErrors.GraphQlError.name(), gqle.getMessage());
170        }
171
172        return result;
173    }
174    
175}