Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(rest): Add support for storing GraphQL/REST Connector's response as a document #3746

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import java.io.InputStream;
import java.time.Duration;
import java.util.Map;
import java.util.UUID;

public record DocumentCreationRequest(
InputStream content,
Expand All @@ -45,7 +46,7 @@ public static class BuilderFinalStep {
private String documentId;
private String storeId;
private String contentType;
private String fileName;
private String fileName = UUID.randomUUID().toString();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets let the client decide on the filename and not do it here.

private Duration timeToLive;
private Map<String, Object> customProperties;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ public void clear() {
documents.clear();
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

found it helpful for test purposes, and this is the InMemory store not a production one, I guess it's ok

public Map<String, byte[]> getDocuments() {
return documents;
}

public void logWarning() {
LOGGER.warning(
"In-memory document store is used. This store is not suitable for production use.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"id" : "io.camunda.connectors.GraphQL.v1",
"description" : "Execute GraphQL query",
"documentationRef" : "https://docs.camunda.io/docs/components/connectors/protocol/graphql/",
"version" : 6,
"version" : 7,
"category" : {
"id" : "connectors",
"name" : "Connectors"
Expand Down Expand Up @@ -366,6 +366,18 @@
"type" : "zeebe:input"
},
"type" : "String"
}, {
"id" : "graphql.storeResponse",
"label" : "Store response",
"description" : "Store the response as a document in the document store",
"optional" : false,
"value" : false,
"group" : "endpoint",
"binding" : {
"name" : "graphql.storeResponse",
"type" : "zeebe:input"
},
"type" : "Boolean"
}, {
"id" : "graphql.query",
"label" : "Query/Mutation",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"id" : "io.camunda.connectors.GraphQL.v1-hybrid",
"description" : "Execute GraphQL query",
"documentationRef" : "https://docs.camunda.io/docs/components/connectors/protocol/graphql/",
"version" : 6,
"version" : 7,
"category" : {
"id" : "connectors",
"name" : "Connectors"
Expand Down Expand Up @@ -371,6 +371,18 @@
"type" : "zeebe:input"
},
"type" : "String"
}, {
"id" : "graphql.storeResponse",
"label" : "Store response",
"description" : "Store the response as a document in the document store",
"optional" : false,
"value" : false,
"group" : "endpoint",
"binding" : {
"name" : "graphql.storeResponse",
"type" : "zeebe:input"
},
"type" : "Boolean"
}, {
"id" : "graphql.query",
"label" : "Query/Mutation",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
name = "GraphQL Outbound Connector",
description = "Execute GraphQL query",
inputDataClass = GraphQLRequest.class,
version = 6,
version = 7,
propertyGroups = {
@ElementTemplate.PropertyGroup(id = "authentication", label = "Authentication"),
@ElementTemplate.PropertyGroup(id = "endpoint", label = "HTTP Endpoint"),
Expand Down Expand Up @@ -59,6 +59,6 @@ public Object execute(OutboundConnectorContext context) {
var graphQLRequest = context.bindVariables(GraphQLRequest.class);
HttpCommonRequest commonRequest = graphQLRequestMapper.toHttpCommonRequest(graphQLRequest);
LOGGER.debug("Executing graphql connector with request {}", commonRequest);
return httpService.executeConnectorRequest(commonRequest);
return httpService.executeConnectorRequest(commonRequest, context);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,14 @@ public record GraphQL(
optional = true,
description = "Map of HTTP headers to add to the request")
Map<String, String> headers,
@TemplateProperty(
group = "endpoint",
type = TemplateProperty.PropertyType.Boolean,
feel = Property.FeelMode.disabled,
defaultValueType = TemplateProperty.DefaultValueType.Boolean,
defaultValue = "false",
description = "Store the response as a document in the document store")
boolean storeResponse,
@TemplateProperty(
group = "timeout",
defaultValue = "20",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public HttpCommonRequest toHttpCommonRequest(GraphQLRequest graphQLRequest) {
httpCommonRequest.setQueryParameters(mapQueryAndVariablesToQueryParams(queryAndVariablesMap));
}

httpCommonRequest.setStoreResponse(graphQLRequest.graphql().storeResponse());
httpCommonRequest.setHeaders(graphQLRequest.graphql().headers());
httpCommonRequest.setAuthentication(graphQLRequest.authentication());
httpCommonRequest.setUrl(graphQLRequest.graphql().url());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH
* under one or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information regarding copyright
* ownership. Camunda licenses this file to you under the Apache License,
* Version 2.0; 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.camunda.connector.http.base;

import io.camunda.connector.api.outbound.OutboundConnectorContext;

public sealed interface ExecutionEnvironment
permits ExecutionEnvironment.SaaSCallerSideEnvironment,
ExecutionEnvironment.SaaSCloudFunctionSideEnvironment,
ExecutionEnvironment.SelfManagedEnvironment {

/**
* Indicates whether the option to store the response as a document was selected in the Element
* Template.
*/
boolean storeResponseSelected();

/**
* The connector is executed in the context of the cloud function. This is where the
* HttpCommonRequest will be executed.
*/
record SaaSCloudFunctionSideEnvironment(boolean storeResponseSelected)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe the Environment part of the name is redundant, let me know

implements ExecutionEnvironment {}

/**
* The connector is executed in the context of the caller, i.e. in the C8 Cluster. When executed
* here, the initial HttpCommonRequest will be serialized as JSON and passed to the Cloud
* Function.
*/
record SaaSCallerSideEnvironment(boolean storeResponseSelected, OutboundConnectorContext context)
implements ExecutionEnvironment, StoresDocument {}

record SelfManagedEnvironment(boolean storeResponseSelected, OutboundConnectorContext context)
implements ExecutionEnvironment, StoresDocument {}

/**
* Factory method to create an ExecutionEnvironment based on the given parameters.
*
* @param cloudFunctionEnabled whether the connector is executed in the context of a cloud
* @param isRunningInCloudFunction whether the connector is executed in the cloud function
* @param storeResponseSelected whether the response should be stored as a Document (this property
* comes from the Element Template)
*/
static ExecutionEnvironment from(
boolean cloudFunctionEnabled,
boolean isRunningInCloudFunction,
boolean storeResponseSelected,
OutboundConnectorContext context) {
if (cloudFunctionEnabled) {
return new SaaSCallerSideEnvironment(storeResponseSelected, context);
}
if (isRunningInCloudFunction) {
return new SaaSCloudFunctionSideEnvironment(storeResponseSelected);
}
return new SelfManagedEnvironment(storeResponseSelected, context);
}

interface StoresDocument {
OutboundConnectorContext context();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@
package io.camunda.connector.http.base;

import io.camunda.connector.api.error.ConnectorException;
import io.camunda.connector.api.outbound.OutboundConnectorContext;
import io.camunda.connector.http.base.blocklist.DefaultHttpBlocklistManager;
import io.camunda.connector.http.base.blocklist.HttpBlockListManager;
import io.camunda.connector.http.base.client.HttpClient;
import io.camunda.connector.http.base.client.apache.CustomApacheHttpClient;
import io.camunda.connector.http.base.cloudfunction.CloudFunctionService;
import io.camunda.connector.http.base.model.HttpCommonRequest;
import io.camunda.connector.http.base.model.HttpCommonResult;
import javax.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -45,25 +47,36 @@ public HttpService(CloudFunctionService cloudFunctionService) {
}

public HttpCommonResult executeConnectorRequest(HttpCommonRequest request) {
return executeConnectorRequest(request, null);
}

public HttpCommonResult executeConnectorRequest(
HttpCommonRequest request, @Nullable OutboundConnectorContext context) {
// Will throw ConnectorInputException if URL is blocked
httpBlocklistManager.validateUrlAgainstBlocklist(request.getUrl());
boolean cloudFunctionEnabled = cloudFunctionService.isCloudFunctionEnabled();
ExecutionEnvironment executionEnvironment =
ExecutionEnvironment.from(
cloudFunctionService.isCloudFunctionEnabled(),
cloudFunctionService.isRunningInCloudFunction(),
request.isStoreResponse(),
context);

if (cloudFunctionEnabled) {
if (executionEnvironment instanceof ExecutionEnvironment.SaaSCallerSideEnvironment) {
// Wrap the request in a proxy request
request = cloudFunctionService.toCloudFunctionRequest(request);
}
return executeRequest(request, cloudFunctionEnabled);
return executeRequest(request, executionEnvironment);
}

private HttpCommonResult executeRequest(HttpCommonRequest request, boolean cloudFunctionEnabled) {
private HttpCommonResult executeRequest(
HttpCommonRequest request, @Nullable ExecutionEnvironment executionEnvironment) {
try {
HttpCommonResult jsonResult = httpClient.execute(request, cloudFunctionEnabled);
HttpCommonResult jsonResult = httpClient.execute(request, executionEnvironment);
LOGGER.debug("Connector returned result: {}", jsonResult);
return jsonResult;
} catch (ConnectorException e) {
LOGGER.debug("Failed to execute request {}", request, e);
if (cloudFunctionEnabled) {
if (executionEnvironment instanceof ExecutionEnvironment.SaaSCallerSideEnvironment) {
throw cloudFunctionService.parseCloudFunctionError(e);
}
throw e;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@
*/
package io.camunda.connector.http.base.client;

import io.camunda.connector.http.base.ExecutionEnvironment;
import io.camunda.connector.http.base.model.HttpCommonRequest;
import io.camunda.connector.http.base.model.HttpCommonResult;
import javax.annotation.Nullable;

public interface HttpClient {

Expand All @@ -26,20 +28,21 @@ public interface HttpClient {
* HttpCommonResult}.
*
* @param request the {@link HttpCommonRequest} to execute
* @param remoteExecutionEnabled whether to use the internal Google Function to execute the
* request remotely
* @param executionEnvironment the {@link ExecutionEnvironment} to use for the execution.
* @return the result of the request as a {@link HttpCommonResult}
*/
HttpCommonResult execute(HttpCommonRequest request, boolean remoteExecutionEnabled);
HttpCommonResult execute(
HttpCommonRequest request, @Nullable ExecutionEnvironment executionEnvironment);

/**
* Executes the given {@link HttpCommonRequest} and returns the result as a {@link
* HttpCommonResult}.
*
* @param request the {@link HttpCommonRequest} to execute
* @return the result of the request as a {@link HttpCommonResult}
* @see #execute(HttpCommonRequest, ExecutionEnvironment)
*/
default HttpCommonResult execute(HttpCommonRequest request) {
return execute(request, false);
return execute(request, null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@
package io.camunda.connector.http.base.client.apache;

import io.camunda.connector.api.error.ConnectorException;
import io.camunda.connector.http.base.ExecutionEnvironment;
import io.camunda.connector.http.base.client.HttpClient;
import io.camunda.connector.http.base.client.HttpStatusHelper;
import io.camunda.connector.http.base.exception.ConnectorExceptionMapper;
import io.camunda.connector.http.base.model.HttpCommonRequest;
import io.camunda.connector.http.base.model.HttpCommonResult;
import java.io.IOException;
import javax.annotation.Nullable;
import org.apache.hc.client5.http.ClientProtocolException;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
Expand Down Expand Up @@ -89,12 +91,12 @@ private static PoolingHttpClientConnectionManager createConnectionManager() {
* org.apache.hc.core5.http.ClassicHttpRequest} and executes it.
*
* @param request the request to execute
* @param remoteExecutionEnabled whether to use the internal Google Function to execute the
* request remotely
* @param executionEnvironment the {@link ExecutionEnvironment} we are in
* @return the {@link HttpCommonResult}
*/
@Override
public HttpCommonResult execute(HttpCommonRequest request, boolean remoteExecutionEnabled) {
public HttpCommonResult execute(
HttpCommonRequest request, @Nullable ExecutionEnvironment executionEnvironment) {
var apacheRequest = ApacheRequestFactory.get().createHttpRequest(request);
try {
var result =
Expand All @@ -104,7 +106,7 @@ public HttpCommonResult execute(HttpCommonRequest request, boolean remoteExecuti
// (http.proxyHost, http.proxyPort, etc)
.useSystemProperties()
.build()
.execute(apacheRequest, new HttpCommonResultResponseHandler(remoteExecutionEnabled));
.execute(apacheRequest, new HttpCommonResultResponseHandler(executionEnvironment));
if (HttpStatusHelper.isError(result.status())) {
throw ConnectorExceptionMapper.from(result);
}
Expand Down
Loading
Loading