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