All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.stargate.sgv2.graphql.web.resources.GraphqlResourceBase Maven / Gradle / Ivy

There is a newer version: 2.0.0-ALPHA-17
Show newest version
/*
 * Copyright The Stargate Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.stargate.sgv2.graphql.web.resources;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.MoreObjects;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import graphql.ExecutionInput;
import graphql.GraphQL;
import graphql.GraphqlErrorException;
import io.stargate.bridge.proto.Schema.SchemaRead.SourceApi;
import io.stargate.sgv2.common.grpc.SchemaReads;
import io.stargate.sgv2.common.grpc.StargateBridgeClient;
import io.stargate.sgv2.graphql.web.models.GraphqlJsonBody;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.container.AsyncResponse;
import javax.ws.rs.container.Suspended;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import org.glassfish.jersey.media.multipart.FormDataBodyPart;
import org.glassfish.jersey.media.multipart.FormDataMultiPart;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Factors common logic to handle GraphQL queries from JAX-RS resources.
 *
 * @see Serving (GraphQL) over HTTP
 */
@Produces(MediaType.APPLICATION_JSON)
public class GraphqlResourceBase {

  private static final Logger LOG = LoggerFactory.getLogger(GraphqlResourceBase.class);
  protected static final String APPLICATION_GRAPHQL = "application/graphql";
  private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
  private static final TypeReference>> FILES_MAPPING_TYPE =
      new TypeReference>>() {};
  private static final Splitter PATH_SPLITTER = Splitter.on(".");

  @Inject protected GraphqlCache graphqlCache;

  /**
   * Handles a GraphQL GET request.
   *
   * 

The payload is provided via URL parameters. */ protected void get( String query, String operationName, String variables, GraphQL graphql, HttpServletRequest httpRequest, AsyncResponse asyncResponse, StargateBridgeClient bridge) { if (Strings.isNullOrEmpty(query)) { throw graphqlError(Status.BAD_REQUEST, "You must provide a GraphQL query as a URL parameter"); } try { ExecutionInput.Builder input = ExecutionInput.newExecutionInput(query) .operationName(operationName) .context(new StargateGraphqlContext(httpRequest, bridge, graphqlCache)); if (!Strings.isNullOrEmpty(variables)) { @SuppressWarnings("unchecked") Map parsedVariables = OBJECT_MAPPER.readValue(variables, Map.class); input = input.variables(parsedVariables); } executeAsync(input.build(), graphql, asyncResponse); } catch (IOException e) { throw graphqlError(Status.BAD_REQUEST, "Could not parse variables: " + e.getMessage()); } } /** * Handles a GraphQL POST request that uses the {@link MediaType#APPLICATION_JSON} content type. * *

Such a request normally comprises a JSON-encoded body, but the spec also allows the query to * be passed as a URL parameter. */ protected void postJson( GraphqlJsonBody jsonBody, String queryFromUrl, GraphQL graphql, HttpServletRequest httpRequest, AsyncResponse asyncResponse, StargateBridgeClient bridge) { queryFromUrl = Strings.emptyToNull(queryFromUrl); String queryFromBody = (jsonBody == null) ? null : Strings.emptyToNull(jsonBody.getQuery()); String operationName = (jsonBody == null) ? null : Strings.emptyToNull(jsonBody.getOperationName()); Map variables = (jsonBody == null) ? null : jsonBody.getVariables(); if (queryFromBody == null && queryFromUrl == null) { throw graphqlError( Status.BAD_REQUEST, "You must provide a GraphQL query, either as a query parameter or in the request body"); } if (queryFromBody != null && queryFromUrl != null) { // The GraphQL spec doesn't specify what to do in this case, but it's probably better to error // out rather than pick one arbitrarily. throw graphqlError( Status.BAD_REQUEST, "You can't provide a GraphQL query both as a query parameter and in the request body"); } String query = MoreObjects.firstNonNull(queryFromBody, queryFromUrl); ExecutionInput.Builder input = ExecutionInput.newExecutionInput(query) .operationName(operationName) .context(new StargateGraphqlContext(httpRequest, bridge, graphqlCache)); if (variables != null) { input = input.variables(variables); } executeAsync(input.build(), graphql, asyncResponse); } /** * Handles a GraphQL POST request that uses the {@link MediaType#MULTIPART_FORM_DATA} content * type, allowing file arguments. * *

It follows the GraphQL multipart request * specification. * *

Example cURL call: * *

   * curl http://host:port/path/to/graphql \
   *   -F operations='{ "query": "query ($file: Upload!) { someQuery(file: $file) }", "variables": { "file": null } };type=application/json'
   *   -F map='{ "filePart": ["variables.file"] }'
   *   -FfilePart=@/path/to/file.txt
   * 
* * The first part MUST declare a JSON content type ("type=application/json" in the example above). * *

This method assumes that its argument come from properly annotated arguments in a Jersey * resource; see the existing callers for an example. * * @param jsonBody the JSON payload containing the GraphQL "operations object" to execute (eg * {query=..., variables=...}). It's parsed from the operations * part in the request. * @param allParts the whole multipart request, consisting of: the operations part * (ignored since we've already parsed it above), a map part containing a map * that specifies which GraphQL variable each file corresponds to, and an arbitrary number of * file parts named after the keys of the files map. * @param graphql the GraphQL schema to use for execution. */ protected void postMultipartJson( GraphqlJsonBody jsonBody, FormDataMultiPart allParts, GraphQL graphql, HttpServletRequest httpRequest, AsyncResponse asyncResponse, StargateBridgeClient bridge) { if (jsonBody == null) { throw graphqlError( Status.BAD_REQUEST, "Could not find GraphQL operations object. " + "Make sure your multipart request includes an 'operations' part with MIME type " + MediaType.APPLICATION_JSON); } bindFilesToVariables(jsonBody, allParts, asyncResponse); postJson( jsonBody, // We don't allow passing the query as a URL param for this variant. The spec does not // preclude it explicitly, but it's unlikely that someone would try to do that. null, graphql, httpRequest, asyncResponse, bridge); } /** * Given: * *

    *
  • a GraphQL query such as: *
       *       { "query": "...", "variables": { "file1": , "file2":  } }
       *       
    *
  • a 'map' part such as *
       *       "part1": [ "variables.file1" ], "part2": [ "variables.file2" ] }
       *       
    *
  • two parts 'part1' and 'part2' with the contents of the corresponding files. *
* *

We want to read each file part as an {@link InputStream}, and inject it in {@link * GraphqlJsonBody#getVariables()} at the corresponding position (overriding whatever was there). */ private static void bindFilesToVariables( GraphqlJsonBody jsonBody, FormDataMultiPart allParts, AsyncResponse asyncResponse) { Map variables = jsonBody.getVariables(); FormDataBodyPart filesMappingPart = allParts.getField("map"); if (filesMappingPart != null) { if (variables == null || variables.isEmpty()) { // We could just ignore the files but this is likely a user mistake throw graphqlError( Status.BAD_REQUEST, "Found a 'map' part but the GraphQL query has no variables"); } Map> filesMapping; try { filesMapping = OBJECT_MAPPER.readValue(filesMappingPart.getValue(), FILES_MAPPING_TYPE); } catch (JsonProcessingException e) { throw graphqlError(Status.BAD_REQUEST, "Could not parse map part: " + e.getMessage()); } for (Map.Entry> entry : filesMapping.entrySet()) { String partName = entry.getKey(); List variablePaths = entry.getValue(); FormDataBodyPart part = allParts.getField(partName); if (part == null) { throw graphqlError( Status.BAD_REQUEST, String.format( "The 'map' part references '%s', but found no part with that name", partName)); } if (variablePaths == null || variablePaths.size() != 1) { // The spec allows more than one variable, but we won't use that feature and it would // complicate things with InputStream. String message = String.format( "This implementation only allows file parts to reference exactly one variable " + "(offending part: '%s' with %d variables)", partName, variablePaths == null ? 0 : variablePaths.size()); throw graphqlError(Status.BAD_REQUEST, message); } String variablePath = variablePaths.get(0); List pathElements = PATH_SPLITTER.splitToList(variablePath); if (pathElements.size() != 2 && !"variables".equals(pathElements.get(0))) { // Again, the spec allows more complicated cases like nested variables or arrays, but we // won't need that so let's keep it simple for now. throw graphqlError( Status.BAD_REQUEST, String.format( "This implementation only allows simple variable references like 'variables.x' " + "(offending reference: '%s')", variablePath)); } String variableName = pathElements.get(1); variables.put(variableName, part.getEntityAs(InputStream.class)); } } } /** * Handles a GraphQL POST request that uses the "application/graphql" content type. * *

The request body is the GraphQL query directly. */ protected void postGraphql( String query, GraphQL graphql, HttpServletRequest httpRequest, AsyncResponse asyncResponse, StargateBridgeClient bridge) { if (Strings.isNullOrEmpty(query)) { throw graphqlError( Status.BAD_REQUEST, "You must provide a GraphQL query in the request body"); } ExecutionInput input = ExecutionInput.newExecutionInput(query) .context(new StargateGraphqlContext(httpRequest, bridge, graphqlCache)) .build(); executeAsync(input, graphql, asyncResponse); } protected static void executeAsync( ExecutionInput input, GraphQL graphql, @Suspended AsyncResponse asyncResponse) { graphql .executeAsync(input) .whenComplete( (result, error) -> { if (error != null) { LOG.error("Unexpected error while processing GraphQL request", error); throw graphqlError(Status.INTERNAL_SERVER_ERROR, "Internal server error"); } else { StargateGraphqlContext context = (StargateGraphqlContext) input.getContext(); if (context.isOverloaded()) { throw graphqlError(Status.TOO_MANY_REQUESTS, "Database is overloaded"); } else { asyncResponse.resume(result.toSpecification()); } } }); } protected boolean isAuthorized(String keyspaceName, StargateBridgeClient bridge) { return bridge.authorizeSchemaRead(SchemaReads.keyspace(keyspaceName, SourceApi.GRAPHQL)); } protected static WebApplicationException graphqlError(Status status, String message) { return new WebApplicationException( Response.status(status) .entity( ImmutableMap.of("errors", ImmutableList.of(ImmutableMap.of("message", message)))) .build()); } protected static WebApplicationException graphqlError( Status status, GraphqlErrorException error) { return new WebApplicationException( Response.status(status) .entity(ImmutableMap.of("errors", ImmutableList.of(error.toSpecification()))) .build()); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy