001// --------------------------------------------------------------------------------
002// Copyright 2002-2024 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.ExecuteGraphQlResult;
021import com.echothree.control.user.graphql.common.result.GraphQlResultFactory;
022import com.echothree.model.control.graphql.server.util.BaseGraphQl;
023import com.echothree.model.control.graphql.server.util.GraphQlExecutionContext;
024import com.echothree.model.control.graphql.server.util.GraphQlSchemaUtils;
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.GraphQLContext;
037import graphql.GraphQLException;
038import graphql.annotations.strategies.EnhancedExecutionStrategy;
039import java.util.Arrays;
040import java.util.Collections;
041import java.util.LinkedHashMap;
042import java.util.List;
043import java.util.Map;
044
045public class ExecuteGraphQlCommand
046        extends BaseSimpleCommand<ExecuteGraphQlForm> {
047    
048    private final static List<FieldDefinition> FORM_FIELD_DEFINITIONS;
049    
050    static {
051        FORM_FIELD_DEFINITIONS = Collections.unmodifiableList(Arrays.asList(
052                new FieldDefinition("ReadOnly", FieldType.BOOLEAN, true, null, null),
053                new FieldDefinition("Query", FieldType.STRING, false, 1L, null),
054                new FieldDefinition("Variables", FieldType.STRING, false, 1L, null),
055                new FieldDefinition("OperationName", FieldType.STRING, false, 1L, null),
056                new FieldDefinition("Json", FieldType.STRING, false, 1L, null),
057                // RemoteInet4Address is purposefully validated only as a string, as it's passed
058                // to other UCs that will validate it as an IPv4 address and format as necessary
059                // for use.
060                new FieldDefinition("RemoteInet4Address", FieldType.STRING, false, 1L, null)
061                ));
062    }
063    
064    /** Creates a new instance of ExecuteGraphQlCommand */
065    public ExecuteGraphQlCommand(UserVisitPK userVisitPK, ExecuteGraphQlForm form) {
066        super(userVisitPK, form, null, FORM_FIELD_DEFINITIONS, false);
067    }
068    
069    private static final String GRAPHQL_QUERY = "query";
070    private static final String GRAPHQL_OPERATION_NAME = "operationName";
071    private static final String GRAPHQL_VARIABLES = "variables";
072    
073    public String toJson(ExecutionResult executionResult)  {
074        // Contents of the GraphQL Response are specified here:
075        // http://graphql.org/learn/serving-over-http/
076        Map<String, Object> executionResultMap = new LinkedHashMap<>();
077        
078        if (executionResult.getErrors().size() > 0) {
079            executionResultMap.put("errors", executionResult.getErrors());
080        }
081        executionResultMap.put("data", executionResult.getData());
082        
083        return GraphQlUtils.getInstance().toJson(executionResultMap);
084    }
085    
086    @Override
087    protected BaseResult execute() {
088        ExecuteGraphQlResult result = GraphQlResultFactory.getExecuteGraphQlResult();
089
090        try {
091            boolean readOnly = Boolean.parseBoolean(form.getReadOnly());
092            String query = form.getQuery();
093            String variables = form.getVariables();
094            String operationName = form.getOperationName();
095            String json = form.getJson();
096
097            GraphQL graphQL = GraphQL
098                    .newGraphQL(readOnly? GraphQlSchemaUtils.getInstance().getReadOnlySchema() : GraphQlSchemaUtils.getInstance().getSchema())
099                    .queryExecutionStrategy(new EnhancedExecutionStrategy())
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                Map<String, Object> body = GraphQlUtils.getInstance().toMap(json);
115                Object possibleQuery = body.get(GRAPHQL_QUERY);
116                Object possibleOperationName = body.get(GRAPHQL_OPERATION_NAME);
117                Object 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) {
122                        query = (String)possibleQuery;
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) {
131                        operationName = (String)possibleOperationName;
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(), 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}