diff --git a/bundle/mvn/default-bundle/pom.xml b/bundle/mvn/default-bundle/pom.xml
index 20f1ef5422..5828127f84 100644
--- a/bundle/mvn/default-bundle/pom.xml
+++ b/bundle/mvn/default-bundle/pom.xml
@@ -60,6 +60,10 @@
io.camunda.connector
connector-microsoft-teams
+
+ io.camunda.connector
+ connector-graphql
+
diff --git a/bundle/mvn/pom.xml b/bundle/mvn/pom.xml
index 0403b4fa9b..e08fe64b5c 100644
--- a/bundle/mvn/pom.xml
+++ b/bundle/mvn/pom.xml
@@ -74,6 +74,11 @@
connector-slack
${project.version}
+
+ io.camunda.connector
+ connector-graphql
+ ${project.version}
+
diff --git a/connector-archetype-internal/src/main/resources/archetype-resources/pom.xml b/connector-archetype-internal/src/main/resources/archetype-resources/pom.xml
index 025a656a07..6761ab5e81 100644
--- a/connector-archetype-internal/src/main/resources/archetype-resources/pom.xml
+++ b/connector-archetype-internal/src/main/resources/archetype-resources/pom.xml
@@ -11,7 +11,7 @@
${artifactId}
- ${connectorName} cnnector for Camunda 8
+ ${connectorName} connector for Camunda 8
${artifactId}
jar
diff --git a/connectors/connectors-common-library/pom.xml b/connectors/connectors-common-library/pom.xml
new file mode 100644
index 0000000000..9404a9c4e4
--- /dev/null
+++ b/connectors/connectors-common-library/pom.xml
@@ -0,0 +1,67 @@
+
+ 4.0.0
+
+
+ io.camunda.connector
+ connector-function-parent
+ 0.16.0-SNAPSHOT
+ ../pom.xml
+
+
+ connectors-common-library
+ Common library for connectors
+ connectors-common-library
+ jar
+
+
+ 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.
+
+
+
+
+ org.danilopianini
+ gson-extras
+
+
+
+ com.google.http-client
+ google-http-client-gson
+
+
+
+ com.google.http-client
+ google-http-client-apache-v2
+
+
+
+ com.google.auth
+ google-auth-library-oauth2-http
+
+
+
+ org.apache.httpcomponents
+ httpcore
+
+
+
+ org.slf4j
+ jcl-over-slf4j
+
+
+
+
\ No newline at end of file
diff --git a/connectors/http-json/src/main/java/io/camunda/connector/http/auth/Authentication.java b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/auth/Authentication.java
similarity index 97%
rename from connectors/http-json/src/main/java/io/camunda/connector/http/auth/Authentication.java
rename to connectors/connectors-common-library/src/main/java/io/camunda/connector/common/auth/Authentication.java
index ff37b792ab..20086a80c5 100644
--- a/connectors/http-json/src/main/java/io/camunda/connector/http/auth/Authentication.java
+++ b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/auth/Authentication.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package io.camunda.connector.http.auth;
+package io.camunda.connector.common.auth;
import com.google.api.client.http.HttpHeaders;
import com.google.common.base.Objects;
diff --git a/connectors/http-json/src/main/java/io/camunda/connector/http/auth/BasicAuthentication.java b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/auth/BasicAuthentication.java
similarity index 98%
rename from connectors/http-json/src/main/java/io/camunda/connector/http/auth/BasicAuthentication.java
rename to connectors/connectors-common-library/src/main/java/io/camunda/connector/common/auth/BasicAuthentication.java
index ac8ee62bc6..91a10b5bad 100644
--- a/connectors/http-json/src/main/java/io/camunda/connector/http/auth/BasicAuthentication.java
+++ b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/auth/BasicAuthentication.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package io.camunda.connector.http.auth;
+package io.camunda.connector.common.auth;
import com.google.api.client.http.HttpHeaders;
import com.google.common.base.Objects;
diff --git a/connectors/http-json/src/main/java/io/camunda/connector/http/auth/BearerAuthentication.java b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/auth/BearerAuthentication.java
similarity index 97%
rename from connectors/http-json/src/main/java/io/camunda/connector/http/auth/BearerAuthentication.java
rename to connectors/connectors-common-library/src/main/java/io/camunda/connector/common/auth/BearerAuthentication.java
index a82cf35a38..7c95ef2f91 100644
--- a/connectors/http-json/src/main/java/io/camunda/connector/http/auth/BearerAuthentication.java
+++ b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/auth/BearerAuthentication.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package io.camunda.connector.http.auth;
+package io.camunda.connector.common.auth;
import com.google.api.client.http.HttpHeaders;
import com.google.common.base.Objects;
diff --git a/connectors/http-json/src/main/java/io/camunda/connector/http/auth/CustomAuthentication.java b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/auth/CustomAuthentication.java
similarity index 91%
rename from connectors/http-json/src/main/java/io/camunda/connector/http/auth/CustomAuthentication.java
rename to connectors/connectors-common-library/src/main/java/io/camunda/connector/common/auth/CustomAuthentication.java
index 4783f6ebc9..29dd2f40d8 100644
--- a/connectors/http-json/src/main/java/io/camunda/connector/http/auth/CustomAuthentication.java
+++ b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/auth/CustomAuthentication.java
@@ -14,11 +14,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package io.camunda.connector.http.auth;
+package io.camunda.connector.common.auth;
import com.google.api.client.http.HttpHeaders;
import io.camunda.connector.api.annotation.Secret;
-import io.camunda.connector.http.model.HttpJsonRequest;
+import io.camunda.connector.common.model.CommonRequest;
import java.util.Map;
import java.util.Objects;
import javax.validation.Valid;
@@ -26,7 +26,7 @@
public class CustomAuthentication extends Authentication {
- @NotNull @Valid private HttpJsonRequest request;
+ @NotNull @Valid private CommonRequest request;
@Secret private Map outputBody;
@@ -35,11 +35,11 @@ public class CustomAuthentication extends Authentication {
@Override
public void setHeaders(final HttpHeaders headers) {}
- public HttpJsonRequest getRequest() {
+ public CommonRequest getRequest() {
return request;
}
- public void setRequest(final HttpJsonRequest request) {
+ public void setRequest(final CommonRequest request) {
this.request = request;
}
diff --git a/connectors/http-json/src/main/java/io/camunda/connector/http/auth/NoAuthentication.java b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/auth/NoAuthentication.java
similarity index 96%
rename from connectors/http-json/src/main/java/io/camunda/connector/http/auth/NoAuthentication.java
rename to connectors/connectors-common-library/src/main/java/io/camunda/connector/common/auth/NoAuthentication.java
index 5a84625b60..bd8b5ce061 100644
--- a/connectors/http-json/src/main/java/io/camunda/connector/http/auth/NoAuthentication.java
+++ b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/auth/NoAuthentication.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package io.camunda.connector.http.auth;
+package io.camunda.connector.common.auth;
import com.google.api.client.http.HttpHeaders;
diff --git a/connectors/http-json/src/main/java/io/camunda/connector/http/auth/OAuthAuthentication.java b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/auth/OAuthAuthentication.java
similarity index 86%
rename from connectors/http-json/src/main/java/io/camunda/connector/http/auth/OAuthAuthentication.java
rename to connectors/connectors-common-library/src/main/java/io/camunda/connector/common/auth/OAuthAuthentication.java
index af3c9a9d82..ea3370db58 100644
--- a/connectors/http-json/src/main/java/io/camunda/connector/http/auth/OAuthAuthentication.java
+++ b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/auth/OAuthAuthentication.java
@@ -14,13 +14,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package io.camunda.connector.http.auth;
+package io.camunda.connector.common.auth;
import com.google.api.client.http.HttpHeaders;
import io.camunda.connector.api.annotation.Secret;
-import io.camunda.connector.http.constants.Constants;
-import java.util.HashMap;
-import java.util.Map;
import java.util.Objects;
import javax.validation.constraints.NotEmpty;
@@ -31,20 +28,8 @@ public class OAuthAuthentication extends Authentication {
@NotEmpty @Secret private String clientSecret;
@Secret private String audience;
@NotEmpty private String clientAuthentication;
- private String scopes;
-
- public Map getDataForAuthRequestBody() {
- Map data = new HashMap<>();
- data.put(Constants.GRANT_TYPE, grantType);
- data.put(Constants.AUDIENCE, audience);
- data.put(Constants.SCOPE, scopes);
- if (clientAuthentication.equals(Constants.CREDENTIALS_BODY)) {
- data.put(Constants.CLIENT_ID, clientId);
- data.put(Constants.CLIENT_SECRET, clientSecret);
- }
- return data;
- }
+ private String scopes;
public String getClientId() {
return clientId;
diff --git a/connectors/http-json/src/main/java/io/camunda/connector/http/constants/Constants.java b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/constants/Constants.java
similarity index 93%
rename from connectors/http-json/src/main/java/io/camunda/connector/http/constants/Constants.java
rename to connectors/connectors-common-library/src/main/java/io/camunda/connector/common/constants/Constants.java
index 0f29108410..552e88b468 100644
--- a/connectors/http-json/src/main/java/io/camunda/connector/http/constants/Constants.java
+++ b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/constants/Constants.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package io.camunda.connector.http.constants;
+package io.camunda.connector.common.constants;
public class Constants {
public static final String GRANT_TYPE = "grant_type";
@@ -23,7 +23,6 @@ public class Constants {
public static final String AUDIENCE = "audience";
public static final String SCOPE = "scope";
public static final String ACCESS_TOKEN = "access_token";
- public static final String COMPUTE_METADATA = "computeMetadata";
public static final String BASIC_AUTH_HEADER = "basicAuthHeader";
public static final String CREDENTIALS_BODY = "credentialsBody";
public static final String PROXY_FUNCTION_URL_ENV_NAME = "CAMUNDA_CONNECTOR_HTTP_PROXY_URL";
diff --git a/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/model/CommonRequest.java b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/model/CommonRequest.java
new file mode 100644
index 0000000000..6a42de90d9
--- /dev/null
+++ b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/model/CommonRequest.java
@@ -0,0 +1,162 @@
+/*
+ * 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.common.model;
+
+import io.camunda.connector.api.annotation.Secret;
+import io.camunda.connector.common.auth.Authentication;
+import java.util.Map;
+import java.util.Objects;
+import javax.validation.Valid;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.Pattern;
+
+public class CommonRequest {
+
+ @NotBlank
+ @Pattern(regexp = "^(http://|https://|secrets).*$")
+ @Secret
+ private String url;
+
+ @NotBlank @Secret private String method;
+
+ @Valid @Secret private Authentication authentication;
+
+ @Pattern(regexp = "^([0-9]*$)|(secrets.*$)")
+ @Secret
+ private String connectionTimeoutInSeconds;
+
+ @Secret private Map headers;
+
+ @Secret private Object body;
+
+ @Secret private Map queryParameters;
+
+ public Object getBody() {
+ return body;
+ }
+
+ public void setBody(final Object body) {
+ this.body = body;
+ }
+
+ public boolean hasHeaders() {
+ return headers != null;
+ }
+
+ public boolean hasBody() {
+ return body != null;
+ }
+
+ public Map getHeaders() {
+ return headers;
+ }
+
+ public void setHeaders(final Map headers) {
+ this.headers = headers;
+ }
+
+ public boolean hasQueryParameters() {
+ return queryParameters != null;
+ }
+
+ public Map getQueryParameters() {
+ return queryParameters;
+ }
+
+ public void setQueryParameters(Map queryParameters) {
+ this.queryParameters = queryParameters;
+ }
+
+ public boolean hasAuthentication() {
+ return authentication != null;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ public void setUrl(String url) {
+ this.url = url;
+ }
+
+ public Authentication getAuthentication() {
+ return authentication;
+ }
+
+ public void setAuthentication(Authentication authentication) {
+ this.authentication = authentication;
+ }
+
+ public String getMethod() {
+ return method;
+ }
+
+ public void setMethod(String method) {
+ this.method = method;
+ }
+
+ public String getConnectionTimeoutInSeconds() {
+ return connectionTimeoutInSeconds;
+ }
+
+ public void setConnectionTimeoutInSeconds(String connectionTimeoutInSeconds) {
+ this.connectionTimeoutInSeconds = connectionTimeoutInSeconds;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ CommonRequest that = (CommonRequest) o;
+ return url.equals(that.url)
+ && method.equals(that.method)
+ && Objects.equals(authentication, that.authentication)
+ && Objects.equals(connectionTimeoutInSeconds, that.connectionTimeoutInSeconds)
+ && Objects.equals(headers, that.headers)
+ && Objects.equals(body, that.body)
+ && Objects.equals(queryParameters, that.queryParameters);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ url, method, authentication, connectionTimeoutInSeconds, headers, body, queryParameters);
+ }
+
+ @Override
+ public String toString() {
+ return "CommonRequest{"
+ + "url='"
+ + url
+ + '\''
+ + ", method='"
+ + method
+ + '\''
+ + ", authentication="
+ + authentication
+ + ", connectionTimeoutInSeconds='"
+ + connectionTimeoutInSeconds
+ + '\''
+ + ", headers="
+ + headers
+ + ", body="
+ + body
+ + ", queryParameters="
+ + queryParameters
+ + '}';
+ }
+}
diff --git a/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/model/CommonResult.java b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/model/CommonResult.java
new file mode 100644
index 0000000000..3e3fee0f62
--- /dev/null
+++ b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/model/CommonResult.java
@@ -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.common.model;
+
+import com.google.common.base.Objects;
+import java.util.Map;
+
+public class CommonResult {
+
+ private int status;
+ private Map headers;
+ private Object body;
+
+ public int getStatus() {
+ return status;
+ }
+
+ public void setStatus(int status) {
+ this.status = status;
+ }
+
+ public Map getHeaders() {
+ return headers;
+ }
+
+ public void setHeaders(Map headers) {
+ this.headers = headers;
+ }
+
+ public Object getBody() {
+ return body;
+ }
+
+ public void setBody(Object body) {
+ this.body = body;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ CommonResult that = (CommonResult) o;
+ return status == that.status
+ && Objects.equal(headers, that.headers)
+ && Objects.equal(body, that.body);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(status, headers, body);
+ }
+
+ @Override
+ public String toString() {
+ return "GraphQLResult{" + "status=" + status + ", headers=" + headers + ", body=" + body + '}';
+ }
+}
diff --git a/connectors/http-json/src/main/java/io/camunda/connector/http/model/ErrorResponse.java b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/model/ErrorResponse.java
similarity index 97%
rename from connectors/http-json/src/main/java/io/camunda/connector/http/model/ErrorResponse.java
rename to connectors/connectors-common-library/src/main/java/io/camunda/connector/common/model/ErrorResponse.java
index 8f6285f435..9029e14cf7 100644
--- a/connectors/http-json/src/main/java/io/camunda/connector/http/model/ErrorResponse.java
+++ b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/model/ErrorResponse.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package io.camunda.connector.http.model;
+package io.camunda.connector.common.model;
import java.util.Objects;
diff --git a/connectors/http-json/src/main/java/io/camunda/connector/http/model/HttpRequestBuilder.java b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/model/HttpRequestBuilder.java
similarity index 98%
rename from connectors/http-json/src/main/java/io/camunda/connector/http/model/HttpRequestBuilder.java
rename to connectors/connectors-common-library/src/main/java/io/camunda/connector/common/model/HttpRequestBuilder.java
index 6790ff42ac..b904c2b091 100644
--- a/connectors/http-json/src/main/java/io/camunda/connector/http/model/HttpRequestBuilder.java
+++ b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/model/HttpRequestBuilder.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package io.camunda.connector.http.model;
+package io.camunda.connector.common.model;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpContent;
diff --git a/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/services/AuthenticationService.java b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/services/AuthenticationService.java
new file mode 100644
index 0000000000..733bd7820f
--- /dev/null
+++ b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/services/AuthenticationService.java
@@ -0,0 +1,127 @@
+/*
+ * 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.common.services;
+
+import static io.camunda.connector.common.utils.Timeout.setTimeout;
+
+import com.google.api.client.http.GenericUrl;
+import com.google.api.client.http.HttpContent;
+import com.google.api.client.http.HttpHeaders;
+import com.google.api.client.http.HttpRequest;
+import com.google.api.client.http.HttpRequestFactory;
+import com.google.api.client.http.HttpResponse;
+import com.google.api.client.http.UrlEncodedContent;
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import io.camunda.connector.common.auth.CustomAuthentication;
+import io.camunda.connector.common.auth.OAuthAuthentication;
+import io.camunda.connector.common.constants.Constants;
+import io.camunda.connector.common.model.CommonRequest;
+import io.camunda.connector.common.utils.JsonHelper;
+import io.camunda.connector.common.utils.ResponseParser;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class AuthenticationService {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(AuthenticationService.class);
+
+ private final Gson gson;
+ private final HttpRequestFactory requestFactory;
+
+ public AuthenticationService(final Gson gson, final HttpRequestFactory requestFactory) {
+ this.gson = gson;
+ this.requestFactory = requestFactory;
+ }
+
+ public String extractOAuthAccessToken(HttpResponse oauthResponse) throws IOException {
+ return Optional.ofNullable(JsonHelper.getAsJsonElement(oauthResponse.parseAsString(), gson))
+ .map(JsonElement::getAsJsonObject)
+ .map(jsonObject -> jsonObject.get(Constants.ACCESS_TOKEN))
+ .map(JsonElement::getAsString)
+ .orElse(null);
+ }
+
+ public void fillRequestFromCustomAuthResponseData(
+ final CommonRequest request,
+ final CustomAuthentication authentication,
+ final HttpResponse httpResponse)
+ throws IOException {
+ String strResponse = httpResponse.parseAsString();
+ Map headers =
+ ResponseParser.extractPropertiesFromBody(
+ authentication.getOutputHeaders(), strResponse, gson);
+ if (headers != null) {
+ if (!request.hasHeaders()) {
+ request.setHeaders(new HashMap<>());
+ }
+ request.getHeaders().putAll(headers);
+ }
+
+ Map body =
+ ResponseParser.extractPropertiesFromBody(authentication.getOutputBody(), strResponse, gson);
+ if (body != null) {
+ if (!request.hasBody()) {
+ request.setBody(new Object());
+ }
+ JsonObject requestBody = gson.toJsonTree(request.getBody()).getAsJsonObject();
+ // for now, we can add only string property to body, example of this object :
+ // "{"key":"value"}" but we can expand this method
+ body.forEach(requestBody::addProperty);
+ request.setBody(gson.fromJson(gson.toJson(requestBody), Object.class));
+ }
+ }
+
+ public HttpRequest createOAuthRequest(CommonRequest request) throws IOException {
+ OAuthAuthentication authentication = (OAuthAuthentication) request.getAuthentication();
+
+ final GenericUrl genericUrl = new GenericUrl(authentication.getOauthTokenEndpoint());
+ Map data = getDataForAuthRequestBody(authentication);
+ HttpContent content = new UrlEncodedContent(data);
+ final String method = Constants.POST;
+ final var httpRequest = requestFactory.buildRequest(method, genericUrl, content);
+ httpRequest.setFollowRedirects(false);
+ setTimeout(request, httpRequest);
+ HttpHeaders headers = new HttpHeaders();
+
+ if (Constants.BASIC_AUTH_HEADER.equals(authentication.getClientAuthentication())) {
+ headers.setBasicAuthentication(
+ authentication.getClientId(), authentication.getClientSecret());
+ }
+ headers.setContentType(Constants.APPLICATION_X_WWW_FORM_URLENCODED);
+ httpRequest.setHeaders(headers);
+ return httpRequest;
+ }
+
+ private static Map getDataForAuthRequestBody(OAuthAuthentication authentication) {
+ Map data = new HashMap<>();
+ data.put(Constants.GRANT_TYPE, authentication.getGrantType());
+ data.put(Constants.AUDIENCE, authentication.getAudience());
+ data.put(Constants.SCOPE, authentication.getScopes());
+
+ if (Constants.CREDENTIALS_BODY.equals(authentication.getClientAuthentication())) {
+ data.put(Constants.CLIENT_ID, authentication.getClientId());
+ data.put(Constants.CLIENT_SECRET, authentication.getClientSecret());
+ }
+ return data;
+ }
+}
diff --git a/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/services/HTTPProxyService.java b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/services/HTTPProxyService.java
new file mode 100644
index 0000000000..cb423d59b0
--- /dev/null
+++ b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/services/HTTPProxyService.java
@@ -0,0 +1,63 @@
+/*
+ * 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.common.services;
+
+import com.google.api.client.http.AbstractHttpContent;
+import com.google.api.client.http.GenericUrl;
+import com.google.api.client.http.HttpContent;
+import com.google.api.client.http.HttpRequest;
+import com.google.api.client.http.HttpRequestFactory;
+import com.google.gson.Gson;
+import io.camunda.connector.common.constants.Constants;
+import io.camunda.connector.common.model.CommonRequest;
+import io.camunda.connector.common.model.HttpRequestBuilder;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+
+public final class HTTPProxyService {
+
+ public static HttpRequest toRequestViaProxy(
+ final Gson gson,
+ final HttpRequestFactory requestFactory,
+ final CommonRequest request,
+ final String proxyFunctionUrl)
+ throws IOException {
+ // Using the JsonHttpContent cannot work with an element on the root content,
+ // hence write it ourselves:
+ final String contentAsJson = gson.toJson(request);
+ HttpContent content =
+ new AbstractHttpContent(Constants.APPLICATION_JSON_CHARSET_UTF_8) {
+ public void writeTo(OutputStream outputStream) throws IOException {
+ outputStream.write(contentAsJson.getBytes(StandardCharsets.UTF_8));
+ }
+ };
+
+ HttpRequest httpRequest =
+ new HttpRequestBuilder()
+ .method(Constants.POST)
+ .genericUrl(new GenericUrl(proxyFunctionUrl))
+ .content(content)
+ .connectionTimeoutInSeconds(request.getConnectionTimeoutInSeconds())
+ .followRedirects(false)
+ .build(requestFactory);
+
+ ProxyOAuthHelper.addOauthHeaders(
+ httpRequest, ProxyOAuthHelper.initializeCredentials(proxyFunctionUrl));
+ return httpRequest;
+ }
+}
diff --git a/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/services/HTTPService.java b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/services/HTTPService.java
new file mode 100644
index 0000000000..381db705cc
--- /dev/null
+++ b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/services/HTTPService.java
@@ -0,0 +1,118 @@
+/*
+ * 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.common.services;
+
+import static org.apache.http.entity.ContentType.APPLICATION_JSON;
+
+import com.google.api.client.http.HttpHeaders;
+import com.google.api.client.http.HttpRequest;
+import com.google.api.client.http.HttpResponse;
+import com.google.api.client.http.HttpResponseException;
+import com.google.gson.Gson;
+import io.camunda.connector.api.error.ConnectorException;
+import io.camunda.connector.common.constants.Constants;
+import io.camunda.connector.common.model.CommonRequest;
+import io.camunda.connector.common.model.CommonResult;
+import io.camunda.connector.common.model.ErrorResponse;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class HTTPService {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(HTTPService.class);
+
+ private final Gson gson;
+
+ public HTTPService(final Gson gson) {
+ this.gson = gson;
+ }
+
+ public HttpHeaders createHeaders(final CommonRequest request, String bearerToken) {
+ final HttpHeaders httpHeaders = new HttpHeaders();
+ if (Constants.POST.equalsIgnoreCase(request.getMethod())) {
+ httpHeaders.setContentType(APPLICATION_JSON.getMimeType());
+ }
+ if (request.hasAuthentication()) {
+ if (bearerToken != null && !bearerToken.isEmpty()) {
+ httpHeaders.setAuthorization("Bearer " + bearerToken);
+ }
+ request.getAuthentication().setHeaders(httpHeaders);
+ }
+ return httpHeaders;
+ }
+
+ public HttpResponse executeHttpRequest(HttpRequest externalRequest) throws IOException {
+ return executeHttpRequest(externalRequest, false);
+ }
+
+ public HttpResponse executeHttpRequest(HttpRequest externalRequest, boolean isProxyCall)
+ throws IOException {
+ try {
+ return externalRequest.execute();
+ } catch (HttpResponseException hrex) {
+ var errorCode = String.valueOf(hrex.getStatusCode());
+ var errorMessage = hrex.getMessage();
+ if (isProxyCall && hrex.getContent() != null) {
+ try {
+ final var errorContent = gson.fromJson(hrex.getContent(), ErrorResponse.class);
+ errorCode = errorContent.getErrorCode();
+ errorMessage = errorContent.getError();
+ } catch (Exception e) {
+ // cannot be loaded as JSON, ignore and use plain message
+ LOGGER.warn("Error response cannot be parsed as JSON! Will use the plain message.");
+ }
+ }
+ throw new ConnectorException(errorCode, errorMessage, hrex);
+ }
+ }
+
+ public T toHttpJsonResponse(
+ final HttpResponse externalResponse, final Class resultClass)
+ throws InstantiationException, IllegalAccessException {
+ T connectorResult = resultClass.newInstance();
+ connectorResult.setStatus(externalResponse.getStatusCode());
+ final Map headers = new HashMap<>();
+ externalResponse
+ .getHeaders()
+ .forEach(
+ (k, v) -> {
+ if (v instanceof List && ((List>) v).size() == 1) {
+ headers.put(k, ((List>) v).get(0));
+ } else {
+ headers.put(k, v);
+ }
+ });
+ connectorResult.setHeaders(headers);
+ try (InputStream content = externalResponse.getContent();
+ Reader reader = new InputStreamReader(content)) {
+ final Object body = gson.fromJson(reader, Object.class);
+ if (body != null) {
+ connectorResult.setBody(body);
+ }
+ } catch (final Exception e) {
+ LOGGER.error("Failed to parse external response: {}", externalResponse, e);
+ }
+ return connectorResult;
+ }
+}
diff --git a/connectors/http-json/src/main/java/io/camunda/connector/http/auth/ProxyOAuthHelper.java b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/services/ProxyOAuthHelper.java
similarity index 98%
rename from connectors/http-json/src/main/java/io/camunda/connector/http/auth/ProxyOAuthHelper.java
rename to connectors/connectors-common-library/src/main/java/io/camunda/connector/common/services/ProxyOAuthHelper.java
index 030049cff1..a71058258f 100644
--- a/connectors/http-json/src/main/java/io/camunda/connector/http/auth/ProxyOAuthHelper.java
+++ b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/services/ProxyOAuthHelper.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package io.camunda.connector.http.auth;
+package io.camunda.connector.common.services;
import com.google.api.client.http.HttpRequest;
import com.google.auth.oauth2.GoogleCredentials;
diff --git a/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/utils/JsonHelper.java b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/utils/JsonHelper.java
new file mode 100644
index 0000000000..6c7f71af00
--- /dev/null
+++ b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/utils/JsonHelper.java
@@ -0,0 +1,31 @@
+/*
+ * 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.common.utils;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import java.util.Optional;
+
+public class JsonHelper {
+
+ public static JsonElement getAsJsonElement(final String strResponse, final Gson gson) {
+ return Optional.ofNullable(strResponse)
+ .filter(response -> !response.isBlank())
+ .map(response -> gson.fromJson(response, JsonElement.class))
+ .orElse(null);
+ }
+}
diff --git a/connectors/http-json/src/main/java/io/camunda/connector/http/ResponseParser.java b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/utils/ResponseParser.java
similarity index 76%
rename from connectors/http-json/src/main/java/io/camunda/connector/http/ResponseParser.java
rename to connectors/connectors-common-library/src/main/java/io/camunda/connector/common/utils/ResponseParser.java
index 27367fe138..40007d2def 100644
--- a/connectors/http-json/src/main/java/io/camunda/connector/http/ResponseParser.java
+++ b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/utils/ResponseParser.java
@@ -14,40 +14,24 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package io.camunda.connector.http;
+package io.camunda.connector.common.utils;
-import com.google.api.client.http.HttpResponse;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
-import io.camunda.connector.http.components.GsonComponentSupplier;
-import io.camunda.connector.http.constants.Constants;
-import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
-public final class ResponseParser {
-
- private static final Gson gson = GsonComponentSupplier.gsonInstance();
-
- private ResponseParser() {}
-
- public static String extractOAuthAccessToken(HttpResponse oauthResponse) throws IOException {
- return Optional.ofNullable(getAsJsonElement(oauthResponse.parseAsString()))
- .map(JsonElement::getAsJsonObject)
- .map(jsonObject -> jsonObject.get(Constants.ACCESS_TOKEN))
- .map(JsonElement::getAsString)
- .orElse(null);
- }
+public class ResponseParser {
public static Map extractPropertiesFromBody(
- final Map requestedProperties, final String strResponse) {
+ final Map requestedProperties, final String strResponse, final Gson gson) {
if (requestedProperties == null || requestedProperties.isEmpty()) {
return null;
}
final JsonElement asJsonElement =
- Optional.ofNullable(getAsJsonElement(strResponse))
+ Optional.ofNullable(JsonHelper.getAsJsonElement(strResponse, gson))
.orElseThrow(
() -> new IllegalArgumentException("Authentication response body is empty"));
@@ -97,11 +81,4 @@ public static Map extractPropertiesFromBody(
});
return extractedProperties.isEmpty() ? null : extractedProperties;
}
-
- private static JsonElement getAsJsonElement(final String strResponse) {
- return Optional.ofNullable(strResponse)
- .filter(response -> !response.isBlank())
- .map(response -> gson.fromJson(response, JsonElement.class))
- .orElse(null);
- }
}
diff --git a/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/utils/Timeout.java b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/utils/Timeout.java
new file mode 100644
index 0000000000..2f7283ce43
--- /dev/null
+++ b/connectors/connectors-common-library/src/main/java/io/camunda/connector/common/utils/Timeout.java
@@ -0,0 +1,35 @@
+/*
+ * 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.common.utils;
+
+import com.google.api.client.http.HttpRequest;
+import io.camunda.connector.common.model.CommonRequest;
+import java.util.concurrent.TimeUnit;
+
+public class Timeout {
+
+ public static void setTimeout(CommonRequest request, HttpRequest httpRequest) {
+ if (request.getConnectionTimeoutInSeconds() != null) {
+ long connectionTimeout =
+ TimeUnit.SECONDS.toMillis(Long.parseLong(request.getConnectionTimeoutInSeconds()));
+ int intConnectionTimeout = Math.toIntExact(connectionTimeout);
+ httpRequest.setConnectTimeout(intConnectionTimeout);
+ httpRequest.setReadTimeout(intConnectionTimeout);
+ httpRequest.setWriteTimeout(intConnectionTimeout);
+ }
+ }
+}
diff --git a/connectors/graphql/LICENSE.txt b/connectors/graphql/LICENSE.txt
new file mode 100644
index 0000000000..faff62873e
--- /dev/null
+++ b/connectors/graphql/LICENSE.txt
@@ -0,0 +1,5 @@
+Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH under one or more contributor license agreements and licensed to you under a proprietary license.
+You may not use this file except in compliance with the proprietary license.
+The proprietary license can be either the Camunda Platform Self-Managed Free Edition license (available on Camunda’s website) or the Camunda Platform Self-Managed Enterprise Edition license (a copy you obtain when you contact Camunda).
+The Camunda Platform Self-Managed Free Edition comes for free but only allows for usage of the software (file) in non-production environments.
+If you want to use the software (file) in production, you need to purchase the Camunda Platform Self-Managed Enterprise Edition.
diff --git a/connectors/graphql/README.md b/connectors/graphql/README.md
new file mode 100644
index 0000000000..c1135043a4
--- /dev/null
+++ b/connectors/graphql/README.md
@@ -0,0 +1,152 @@
+# Camunda GraphQL Connector
+
+Find the user documentation in our [Camunda Platform 8 Docs](https://docs.camunda.io/docs/components/integration-framework/connectors/out-of-the-box-connectors/graphql/).
+
+## Build
+
+```bash
+mvn clean package
+```
+
+## API
+
+### Input
+
+```json
+{
+ "url": "https://swapi-graphql.netlify.app/.netlify/functions/index",
+ "method": "post",
+ "query": "query Root($id: ID) {person (id: $id) {id, name}}",
+ "variables": "{\"id\": \"cGVvcGxlOjI=\"}"
+}
+```
+
+### Output
+
+The response will contain the status code, the headers and the body of the response of the GraphQL query response.
+
+```json
+{
+ "body": {
+ "data":{
+ "person": {
+ "id":"cGVvcGxlOjI=",
+ "name":"C-3PO"
+ }
+ }
+ },
+ "headers": {
+ "access-control-allow-credentials": "true",
+ "access-control-allow-origin": "*",
+ "connection": "keep-alive",
+ "content-length": 56,
+ "content-type": "application/json; charset=utf-8",
+ "date": "Tue, 15 Mar 2022 21:31:20 GMT",
+ "server": "Netlify"
+ },
+ "status": 200
+}
+```
+
+### Input (Basic)
+
+```json
+{
+ "method": "get",
+ "url": "https://httpbin.org/basic-auth/user/password",
+ "authentication": {
+ "type": "basic",
+ "username": "secrets.USERNAME",
+ "password": "secrets.PASSWORD"
+ }
+}
+```
+
+### Output (Bearer Token)
+
+```json
+{
+ "method": "get",
+ "url": "https://httpbin.org/bearer",
+ "authentication": {
+ "type": "bearer",
+ "token": "secrets.TOKEN"
+ }
+}
+```
+
+### Input (OAuth 2.0)
+
+```json
+{
+ "method": "post",
+ "url": "https://youroauthclientdomainname.eu.auth0.com/oauth/token",
+ "authentication": {
+ "oauthTokenEndpoint":"secrets.OAUTH_TOKEN_ENDPOINT_KEY",
+ "scopes": "read:clients read:users",
+ "audience":"secrets.AUDIENCE_KEY",
+ "clientId":"secrets.CLIENT_ID_KEY",
+ "clientSecret":"secrets.CLIENT_SECRET_KEY",
+ "type": "oauth-client-credentials-flow",
+ "clientAuthentication":"secrets.CLIENT_AUTHENTICATION_KEY"
+ }
+}
+```
+
+### Output (Access Token)
+
+```json
+{
+ "access_token":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IlUtN2N6WG1sMzljUFNfUnlQQkNMWCJ9.kjhwfjkhfejkrhfbwjkfbhetcetc",
+ "scope":"read:clients create:users",
+ "expires_in":86400,
+ "token_type":"Bearer"
+}
+```
+### Error codes
+
+The Connector will fail on any non-2XX HTTP status code in the response. This error status code will be passed on as error code, e.g. "404".
+
+## Use proxy-mechanism
+
+The graphQL connector executes the queries/mutations using an HTTP call. You can configure the GraphQL Connector to do any outgoing HTTP call via a proxy. This proxy should be effectively an HTTP JSON Connector
+running in a different environment.
+
+For example, you can build the following runtime architecture:
+
+```
+ Camunda Process --> GraphQL Connector (Proxy-mode) --> HTTP Connector --> GraphQL Endpoint
+ [ Camunda Network, e.g. K8S ] [ Separate network, e.g. Google Function ]
+```
+
+Now, any GraphQL query/mutation will be just forwarded to a specified hardcoded URL. And this proxy does the real call then.
+This avoids that you could reach internal endpoints in your Camunda network (e.g. the current Kubernetes cluster).
+
+Just set the following property to enable proxy mode for the connector, e.g. in application.properties when using the Spring-based runtime:
+
+```properties
+camunda.connector.http.proxy.url=https://someUrl/
+```
+
+You can also set this via environment variables:
+
+```
+CAMUNDA_CONNECTOR_HTTP_PROXY_URL=https://someUrl/
+```
+
+If the other party requiring OAuth for authentication, you need to set the following environment property:
+
+```shell
+GOOGLE_APPLICATION_CREDENTIALS=...
+```
+
+### :lock: Test the Connector locally with Google Cloud Function as a proxy
+
+Run the [:lock:connector-proxy-saas](https://github.com/camunda/connector-proxy-saas) project locally as described in its [:lock:README](https://github.com/camunda/connector-proxy-saas#usage).
+
+Set the specific property or environment variable to enable proxy mode as described above.
+
+## Element Template
+
+The element templates can be found in
+the [element-templates/graphql-connector.json](element-templates/graphql-connector.json) file.
diff --git a/connectors/graphql/connector.yml b/connectors/graphql/connector.yml
new file mode 100644
index 0000000000..f753d96294
--- /dev/null
+++ b/connectors/graphql/connector.yml
@@ -0,0 +1,3 @@
+name: GRAPHQL
+type: io.camunda:connector-graphql:1
+variables: [ graphql, authentication ]
\ No newline at end of file
diff --git a/connectors/graphql/element-templates/graphql-connector.json b/connectors/graphql/element-templates/graphql-connector.json
new file mode 100644
index 0000000000..62c9e73937
--- /dev/null
+++ b/connectors/graphql/element-templates/graphql-connector.json
@@ -0,0 +1,365 @@
+{
+ "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json",
+ "name": "GraphQL Connector",
+ "id": "io.camunda.connectors.GraphQL.v1",
+ "description": "Execute GraphQL query",
+ "version": 1,
+ "documentationRef": "https://docs.camunda.io/docs/components/connectors/out-of-the-box-connectors/graphql/",
+ "icon": {
+ "contents": "data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' version='1.1' id='GraphQL_Logo' x='0px' y='0px' viewBox='0 0 400 400' enable-background='new 0 0 400 400' xml:space='preserve'%3E%3Cg%3E%3Cg%3E%3Cg%3E%3Crect x='122' y='-0.4' transform='matrix(-0.866 -0.5 0.5 -0.866 163.3196 363.3136)' fill='%23E535AB' width='16.6' height='320.3'/%3E%3C/g%3E%3C/g%3E%3Cg%3E%3Cg%3E%3Crect x='39.8' y='272.2' fill='%23E535AB' width='320.3' height='16.6'/%3E%3C/g%3E%3C/g%3E%3Cg%3E%3Cg%3E%3Crect x='37.9' y='312.2' transform='matrix(-0.866 -0.5 0.5 -0.866 83.0693 663.3409)' fill='%23E535AB' width='185' height='16.6'/%3E%3C/g%3E%3C/g%3E%3Cg%3E%3Cg%3E%3Crect x='177.1' y='71.1' transform='matrix(-0.866 -0.5 0.5 -0.866 463.3409 283.0693)' fill='%23E535AB' width='185' height='16.6'/%3E%3C/g%3E%3C/g%3E%3Cg%3E%3Cg%3E%3Crect x='122.1' y='-13' transform='matrix(-0.5 -0.866 0.866 -0.5 126.7903 232.1221)' fill='%23E535AB' width='16.6' height='185'/%3E%3C/g%3E%3C/g%3E%3Cg%3E%3Cg%3E%3Crect x='109.6' y='151.6' transform='matrix(-0.5 -0.866 0.866 -0.5 266.0828 473.3766)' fill='%23E535AB' width='320.3' height='16.6'/%3E%3C/g%3E%3C/g%3E%3Cg%3E%3Cg%3E%3Crect x='52.5' y='107.5' fill='%23E535AB' width='16.6' height='185'/%3E%3C/g%3E%3C/g%3E%3Cg%3E%3Cg%3E%3Crect x='330.9' y='107.5' fill='%23E535AB' width='16.6' height='185'/%3E%3C/g%3E%3C/g%3E%3Cg%3E%3Cg%3E%3Crect x='262.4' y='240.1' transform='matrix(-0.5 -0.866 0.866 -0.5 126.7953 714.2875)' fill='%23E535AB' width='14.5' height='160.9'/%3E%3C/g%3E%3C/g%3E%3Cpath fill='%23E535AB' d='M369.5,297.9c-9.6,16.7-31,22.4-47.7,12.8c-16.7-9.6-22.4-31-12.8-47.7c9.6-16.7,31-22.4,47.7-12.8 C373.5,259.9,379.2,281.2,369.5,297.9'/%3E%3Cpath fill='%23E535AB' d='M90.9,137c-9.6,16.7-31,22.4-47.7,12.8c-16.7-9.6-22.4-31-12.8-47.7c9.6-16.7,31-22.4,47.7-12.8 C94.8,99,100.5,120.3,90.9,137'/%3E%3Cpath fill='%23E535AB' d='M30.5,297.9c-9.6-16.7-3.9-38,12.8-47.7c16.7-9.6,38-3.9,47.7,12.8c9.6,16.7,3.9,38-12.8,47.7 C61.4,320.3,40.1,314.6,30.5,297.9'/%3E%3Cpath fill='%23E535AB' d='M309.1,137c-9.6-16.7-3.9-38,12.8-47.7c16.7-9.6,38-3.9,47.7,12.8c9.6,16.7,3.9,38-12.8,47.7 C340.1,159.4,318.7,153.7,309.1,137'/%3E%3Cpath fill='%23E535AB' d='M200,395.8c-19.3,0-34.9-15.6-34.9-34.9c0-19.3,15.6-34.9,34.9-34.9c19.3,0,34.9,15.6,34.9,34.9 C234.9,380.1,219.3,395.8,200,395.8'/%3E%3Cpath fill='%23E535AB' d='M200,74c-19.3,0-34.9-15.6-34.9-34.9c0-19.3,15.6-34.9,34.9-34.9c19.3,0,34.9,15.6,34.9,34.9 C234.9,58.4,219.3,74,200,74'/%3E%3C/g%3E%3C/svg%3E"
+ },
+ "category": {
+ "id": "connectors",
+ "name": "Connectors"
+ },
+ "appliesTo": [
+ "bpmn:Task"
+ ],
+ "elementType": {
+ "value": "bpmn:ServiceTask"
+ },
+ "groups": [
+ {
+ "id": "authentication",
+ "label": "Authentication"
+ },
+ {
+ "id": "endpoint",
+ "label": "HTTP Endpoint"
+ },
+ {
+ "id": "graphql",
+ "label": "GraphQL Query"
+ },
+ {
+ "id": "timeout",
+ "label": "Connect Timeout"
+ },
+ {
+ "id": "output",
+ "label": "Response Mapping"
+ },
+ {
+ "id": "errors",
+ "label": "Error Handling"
+ }
+ ],
+ "properties": [
+ {
+ "type": "Hidden",
+ "value": "io.camunda:connector-graphql:1",
+ "binding": {
+ "type": "zeebe:taskDefinition:type"
+ }
+ },
+ {
+ "label": "Type",
+ "id": "authenticationType",
+ "group": "authentication",
+ "description": "Choose the authentication type. Select 'None' if no authentication is necessary",
+ "value": "noAuth",
+ "type": "Dropdown",
+ "choices": [
+ {
+ "name": "None",
+ "value": "noAuth"
+ },
+ {
+ "name": "Basic",
+ "value": "basic"
+ },
+ {
+ "name": "Bearer Token",
+ "value": "bearer"
+ },
+ {
+ "name": "OAuth 2.0",
+ "value": "oauth-client-credentials-flow"
+ }
+ ],
+ "binding": {
+ "type": "zeebe:input",
+ "name": "authentication.type"
+ }
+ },
+ {
+ "label": "URL",
+ "group": "endpoint",
+ "type": "String",
+ "feel": "optional",
+ "binding": {
+ "type": "zeebe:input",
+ "name": "graphql.url"
+ },
+ "constraints": {
+ "notEmpty": true,
+ "pattern": {
+ "value": "^(=|http://|https://|secrets).*$",
+ "message": "Must be a http(s) URL."
+ }
+ }
+ },
+ {
+ "id": "method",
+ "label": "Method",
+ "group": "endpoint",
+ "type": "Dropdown",
+ "value": "post",
+ "choices": [
+ {
+ "name": "POST",
+ "value": "post"
+ },
+ {
+ "name": "GET",
+ "value": "get"
+ }
+ ],
+ "binding": {
+ "type": "zeebe:input",
+ "name": "graphql.method"
+ }
+ },
+ {
+ "label": "Query/Mutation",
+ "description": "See documentation",
+ "group": "graphql",
+ "type": "Text",
+ "language": "graphql",
+ "binding": {
+ "type": "zeebe:input",
+ "name": "graphql.query"
+ },
+ "optional": false,
+ "constraints": {
+ "notEmpty": true
+ }
+ },
+ {
+ "label": "Variables",
+ "description": "Learn how to define variables",
+ "group": "graphql",
+ "type": "Text",
+ "feel": "required",
+ "binding": {
+ "type": "zeebe:input",
+ "name": "graphql.variables"
+ },
+ "optional": true
+ },
+ {
+ "label": "Bearer Token",
+ "group": "authentication",
+ "type": "String",
+ "feel": "optional",
+ "binding": {
+ "type": "zeebe:input",
+ "name": "graphql.authentication.token"
+ },
+ "constraints": {
+ "notEmpty": true
+ },
+ "condition": {
+ "property": "authenticationType",
+ "equals": "bearer"
+ }
+ },
+ {
+ "label": "OAuth Token Endpoint",
+ "description": "The OAuth token endpoint",
+ "group": "authentication",
+ "type": "String",
+ "feel": "optional",
+ "binding": {
+ "type": "zeebe:input",
+ "name": "authentication.oauthTokenEndpoint"
+ },
+ "constraints": {
+ "notEmpty": true
+ },
+ "condition": {
+ "property": "authenticationType",
+ "equals": "oauth-client-credentials-flow"
+ }
+ },
+ {
+ "label": "Client ID",
+ "description": "Your application's Client ID from the OAuth client",
+ "group": "authentication",
+ "type": "String",
+ "feel": "optional",
+ "binding": {
+ "type": "zeebe:input",
+ "name": "authentication.clientId"
+ },
+ "constraints": {
+ "notEmpty": true
+ },
+ "condition": {
+ "property": "authenticationType",
+ "equals": "oauth-client-credentials-flow"
+ }
+ },
+ {
+ "label": "Client secret",
+ "description": "Your application's Client secret from the OAuth client",
+ "group": "authentication",
+ "type": "String",
+ "feel": "optional",
+ "binding": {
+ "type": "zeebe:input",
+ "name": "authentication.clientSecret"
+ },
+ "constraints": {
+ "notEmpty": true
+ },
+ "condition": {
+ "property": "authenticationType",
+ "equals": "oauth-client-credentials-flow"
+ }
+ },
+ {
+ "label": "Scopes",
+ "description": "The scopes which you want to request authorization for (e.g.read:contacts)",
+ "group": "authentication",
+ "type": "String",
+ "feel": "optional",
+ "optional": true,
+ "binding": {
+ "type": "zeebe:input",
+ "name": "authentication.scopes"
+ },
+ "condition": {
+ "property": "authenticationType",
+ "equals": "oauth-client-credentials-flow"
+ }
+ },
+ {
+ "label": "Audience",
+ "description": "The unique identifier of the target API you want to access",
+ "group": "authentication",
+ "type": "String",
+ "feel": "optional",
+ "optional": true,
+ "binding": {
+ "type": "zeebe:input",
+ "name": "authentication.audience"
+ },
+ "condition": {
+ "property": "authenticationType",
+ "equals": "oauth-client-credentials-flow"
+ }
+ },
+ {
+ "label": "Client Authentication",
+ "id": "authenticationType",
+ "group": "authentication",
+ "description": "Send client id and client secret as Basic Auth request in the header, or as client credentials in the request body",
+ "value": "basicAuthHeader",
+ "type": "Dropdown",
+ "choices": [
+ {
+ "name": "Send client credentials in body",
+ "value": "credentialsBody"
+ },
+ {
+ "name": "Send as Basic Auth header",
+ "value": "basicAuthHeader"
+ }
+ ],
+ "binding": {
+ "type": "zeebe:input",
+ "name": "authentication.clientAuthentication"
+ },
+ "condition": {
+ "property": "authenticationType",
+ "equals": "oauth-client-credentials-flow"
+ }
+ },
+ {
+ "label": "Username",
+ "group": "authentication",
+ "type": "String",
+ "feel": "optional",
+ "binding": {
+ "type": "zeebe:input",
+ "name": "authentication.username"
+ },
+ "constraints": {
+ "notEmpty": true
+ },
+ "condition": {
+ "property": "authenticationType",
+ "equals": "basic"
+ }
+ },
+ {
+ "label": "Password",
+ "group": "authentication",
+ "type": "String",
+ "feel": "optional",
+ "binding": {
+ "type": "zeebe:input",
+ "name": "authentication.password"
+ },
+ "constraints": {
+ "notEmpty": true
+ },
+ "condition": {
+ "property": "authenticationType",
+ "equals": "basic"
+ }
+ },
+ {
+ "label": "Connection Timeout",
+ "description": "Sets the timeout in seconds to establish a connection or 0 for an infinite timeout",
+ "group": "timeout",
+ "type": "String",
+ "value": "20",
+ "binding": {
+ "type": "zeebe:input",
+ "name": "graphql.connectionTimeoutInSeconds"
+ },
+ "optional": true,
+ "constraints": {
+ "notEmpty": false,
+ "pattern": {
+ "value": "^([0-9]*$)|(secrets.*$)",
+ "message": "Must be timeout in seconds (default value is 20 seconds)"
+ }
+ }
+ },
+ {
+ "label": "Result Variable",
+ "description": "Name of variable to store the response in",
+ "group": "output",
+ "type": "String",
+ "binding": {
+ "type": "zeebe:taskHeader",
+ "key": "resultVariable"
+ }
+ },
+ {
+ "label": "Result Expression",
+ "description": "Expression to map the response into process variables",
+ "group": "output",
+ "type": "Text",
+ "feel": "required",
+ "binding": {
+ "type": "zeebe:taskHeader",
+ "key": "resultExpression"
+ }
+ },
+ {
+ "label": "Error Expression",
+ "description": "Expression to handle errors. Details in the documentation",
+ "group": "errors",
+ "type": "Text",
+ "feel": "required",
+ "binding": {
+ "type": "zeebe:taskHeader",
+ "key": "errorExpression"
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/connectors/graphql/pom.xml b/connectors/graphql/pom.xml
new file mode 100644
index 0000000000..4ca4cc4d8f
--- /dev/null
+++ b/connectors/graphql/pom.xml
@@ -0,0 +1,70 @@
+
+ 4.0.0
+
+
+ io.camunda.connector
+ connector-function-parent
+ 0.16.0-SNAPSHOT
+ ../pom.xml
+
+
+ connector-graphql
+ graphql connector for Camunda 8
+ connector-graphql
+ jar
+
+
+
+ Camunda Platform Self-Managed Free Edition license
+ https://camunda.com/legal/terms/cloud-terms-and-conditions/camunda-cloud-self-managed-free-edition-terms/
+
+
+ Camunda Platform Self-Managed Enterprise Edition license
+
+
+
+
+
+ org.danilopianini
+ gson-extras
+
+
+
+ com.google.http-client
+ google-http-client-gson
+
+
+
+ com.google.http-client
+ google-http-client-apache-v2
+
+
+
+ com.google.auth
+ google-auth-library-oauth2-http
+
+
+
+ org.apache.httpcomponents
+ httpcore
+
+
+
+ org.slf4j
+ jcl-over-slf4j
+
+
+
+ org.apache.commons
+ commons-text
+
+
+
+ io.camunda.connector
+ connectors-common-library
+
+
+
+
diff --git a/connectors/graphql/src/main/java/io/camunda/connector/graphql/GraphQLFunction.java b/connectors/graphql/src/main/java/io/camunda/connector/graphql/GraphQLFunction.java
new file mode 100644
index 0000000000..9dc668d808
--- /dev/null
+++ b/connectors/graphql/src/main/java/io/camunda/connector/graphql/GraphQLFunction.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH
+ * under one or more contributor license agreements. Licensed under a proprietary license.
+ * See the License.txt file for more information. You may not use this file
+ * except in compliance with the proprietary license.
+ */
+package io.camunda.connector.graphql;
+
+import static io.camunda.connector.common.utils.Timeout.setTimeout;
+
+import com.google.api.client.http.GenericUrl;
+import com.google.api.client.http.HttpContent;
+import com.google.api.client.http.HttpHeaders;
+import com.google.api.client.http.HttpRequest;
+import com.google.api.client.http.HttpRequestFactory;
+import com.google.api.client.http.HttpResponse;
+import com.google.api.client.http.json.JsonHttpContent;
+import com.google.api.client.json.gson.GsonFactory;
+import com.google.gson.Gson;
+import io.camunda.connector.api.annotation.OutboundConnector;
+import io.camunda.connector.api.error.ConnectorException;
+import io.camunda.connector.api.outbound.OutboundConnectorContext;
+import io.camunda.connector.api.outbound.OutboundConnectorFunction;
+import io.camunda.connector.common.auth.OAuthAuthentication;
+import io.camunda.connector.common.constants.Constants;
+import io.camunda.connector.common.model.CommonRequest;
+import io.camunda.connector.common.model.CommonResult;
+import io.camunda.connector.common.services.AuthenticationService;
+import io.camunda.connector.common.services.HTTPProxyService;
+import io.camunda.connector.common.services.HTTPService;
+import io.camunda.connector.graphql.components.GsonComponentSupplier;
+import io.camunda.connector.graphql.components.HttpTransportComponentSupplier;
+import io.camunda.connector.graphql.model.GraphQLRequest;
+import io.camunda.connector.graphql.model.GraphQLResult;
+import io.camunda.connector.graphql.utils.GraphQLRequestMapper;
+import io.camunda.connector.graphql.utils.JsonSerializeHelper;
+import io.camunda.connector.impl.config.ConnectorConfigurationUtil;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.util.Map;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@OutboundConnector(
+ name = "GRAPHQL",
+ inputVariables = {"graphql", "authentication"},
+ type = "io.camunda:connector-graphql:1")
+public class GraphQLFunction implements OutboundConnectorFunction {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(GraphQLFunction.class);
+
+ private final Gson gson;
+ private final GsonFactory gsonFactory;
+ private final HttpRequestFactory requestFactory;
+
+ private final String proxyFunctionUrl;
+
+ public GraphQLFunction() {
+ this(ConnectorConfigurationUtil.getProperty(Constants.PROXY_FUNCTION_URL_ENV_NAME));
+ }
+
+ public GraphQLFunction(String proxyFunctionUrl) {
+ this(
+ GsonComponentSupplier.gsonInstance(),
+ HttpTransportComponentSupplier.httpRequestFactoryInstance(),
+ GsonComponentSupplier.gsonFactoryInstance(),
+ proxyFunctionUrl);
+ }
+
+ public GraphQLFunction(
+ final Gson gson,
+ final HttpRequestFactory requestFactory,
+ final GsonFactory gsonFactory,
+ final String proxyFunctionUrl) {
+ this.gson = gson;
+ this.requestFactory = requestFactory;
+ this.gsonFactory = gsonFactory;
+ this.proxyFunctionUrl = proxyFunctionUrl;
+ }
+
+ @Override
+ public Object execute(OutboundConnectorContext context)
+ throws IOException, InstantiationException, IllegalAccessException {
+ final var json = context.getVariables();
+ var connectorRequest = JsonSerializeHelper.serializeRequest(gson, json);
+ context.validate(connectorRequest);
+ context.replaceSecrets(connectorRequest);
+
+ return StringUtils.isBlank(proxyFunctionUrl)
+ ? executeGraphQLConnector(connectorRequest)
+ : executeGraphQLConnectorViaProxy(connectorRequest);
+ }
+
+ private GraphQLResult executeGraphQLConnector(final GraphQLRequest connectorRequest)
+ throws IOException, InstantiationException, IllegalAccessException {
+ // connector logic
+ LOGGER.debug("Executing graphql connector with request {}", connectorRequest);
+ HTTPService httpService = new HTTPService(gson);
+ AuthenticationService authService = new AuthenticationService(gson, requestFactory);
+ String bearerToken = null;
+ if (connectorRequest.getAuthentication() != null
+ && connectorRequest.getAuthentication() instanceof OAuthAuthentication) {
+ final HttpRequest oauthRequest = authService.createOAuthRequest(connectorRequest);
+ final HttpResponse oauthResponse = httpService.executeHttpRequest(oauthRequest);
+ bearerToken = authService.extractOAuthAccessToken(oauthResponse);
+ }
+
+ final HttpRequest httpRequest = createRequest(httpService, connectorRequest, bearerToken);
+ HttpResponse httpResponse = httpService.executeHttpRequest(httpRequest);
+ return httpService.toHttpJsonResponse(httpResponse, GraphQLResult.class);
+ }
+
+ private CommonResult executeGraphQLConnectorViaProxy(GraphQLRequest request) throws IOException {
+ CommonRequest commonRequest = GraphQLRequestMapper.toCommonRequest(request);
+ HttpRequest httpRequest =
+ HTTPProxyService.toRequestViaProxy(gson, requestFactory, commonRequest, proxyFunctionUrl);
+
+ HTTPService httpService = new HTTPService(gson);
+
+ HttpResponse httpResponse = httpService.executeHttpRequest(httpRequest, true);
+
+ try (InputStream responseContentStream = httpResponse.getContent();
+ Reader reader = new InputStreamReader(responseContentStream)) {
+ final CommonResult jsonResult = gson.fromJson(reader, CommonResult.class);
+ LOGGER.debug("Proxy returned result: " + jsonResult);
+ return jsonResult;
+ } catch (final Exception e) {
+ LOGGER.debug("Failed to parse external response: {}", httpResponse, e);
+ throw new ConnectorException("Failed to parse result: " + e.getMessage(), e);
+ }
+ }
+
+ public HttpRequest createRequest(
+ final HTTPService httpService, final GraphQLRequest request, String bearerToken)
+ throws IOException {
+ final String method = request.getMethod().toUpperCase();
+ final GenericUrl genericUrl = new GenericUrl(request.getUrl());
+ HttpContent content = null;
+ final HttpHeaders headers = httpService.createHeaders(request, bearerToken);
+ final Map queryAndVariablesMap =
+ JsonSerializeHelper.queryAndVariablesToMap(request);
+ if (Constants.POST.equalsIgnoreCase(method)) {
+ content = new JsonHttpContent(gsonFactory, queryAndVariablesMap);
+ } else {
+ genericUrl.putAll(queryAndVariablesMap);
+ }
+
+ final var httpRequest = requestFactory.buildRequest(method, genericUrl, content);
+ httpRequest.setFollowRedirects(false);
+ setTimeout(request, httpRequest);
+ httpRequest.setHeaders(headers);
+
+ return httpRequest;
+ }
+}
diff --git a/connectors/graphql/src/main/java/io/camunda/connector/graphql/components/GsonComponentSupplier.java b/connectors/graphql/src/main/java/io/camunda/connector/graphql/components/GsonComponentSupplier.java
new file mode 100644
index 0000000000..8cbdfdaf50
--- /dev/null
+++ b/connectors/graphql/src/main/java/io/camunda/connector/graphql/components/GsonComponentSupplier.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH
+ * under one or more contributor license agreements. Licensed under a proprietary license.
+ * See the License.txt file for more information. You may not use this file
+ * except in compliance with the proprietary license.
+ */
+package io.camunda.connector.graphql.components;
+
+import com.google.api.client.json.gson.GsonFactory;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.ToNumberPolicy;
+import com.google.gson.typeadapters.RuntimeTypeAdapterFactory;
+import io.camunda.connector.common.auth.Authentication;
+import io.camunda.connector.common.auth.BasicAuthentication;
+import io.camunda.connector.common.auth.BearerAuthentication;
+import io.camunda.connector.common.auth.NoAuthentication;
+import io.camunda.connector.common.auth.OAuthAuthentication;
+
+public class GsonComponentSupplier {
+
+ private static final GsonFactory GSON_FACTORY = new GsonFactory();
+ private static final Gson GSON =
+ new GsonBuilder()
+ .serializeNulls()
+ .setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE)
+ .registerTypeAdapterFactory(
+ RuntimeTypeAdapterFactory.of(Authentication.class, "type")
+ .registerSubtype(NoAuthentication.class, "noAuth")
+ .registerSubtype(BasicAuthentication.class, "basic")
+ .registerSubtype(BearerAuthentication.class, "bearer")
+ .registerSubtype(OAuthAuthentication.class, "oauth-client-credentials-flow"))
+ .create();
+
+ private GsonComponentSupplier() {}
+
+ public static Gson gsonInstance() {
+ return GSON;
+ }
+
+ public static GsonFactory gsonFactoryInstance() {
+ return GSON_FACTORY;
+ }
+}
diff --git a/connectors/graphql/src/main/java/io/camunda/connector/graphql/components/HttpTransportComponentSupplier.java b/connectors/graphql/src/main/java/io/camunda/connector/graphql/components/HttpTransportComponentSupplier.java
new file mode 100644
index 0000000000..df07e6a26d
--- /dev/null
+++ b/connectors/graphql/src/main/java/io/camunda/connector/graphql/components/HttpTransportComponentSupplier.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH
+ * under one or more contributor license agreements. Licensed under a proprietary license.
+ * See the License.txt file for more information. You may not use this file
+ * except in compliance with the proprietary license.
+ */
+package io.camunda.connector.graphql.components;
+
+import com.google.api.client.http.HttpRequestFactory;
+import com.google.api.client.http.HttpTransport;
+import com.google.api.client.http.apache.v2.ApacheHttpTransport;
+import com.google.api.client.json.JsonObjectParser;
+
+public class HttpTransportComponentSupplier {
+
+ private HttpTransportComponentSupplier() {}
+
+ private static final HttpTransport HTTP_TRANSPORT = new ApacheHttpTransport();
+ private static final HttpRequestFactory REQUEST_FACTORY =
+ HTTP_TRANSPORT.createRequestFactory(
+ request ->
+ request.setParser(new JsonObjectParser(GsonComponentSupplier.gsonFactoryInstance())));
+
+ public static HttpRequestFactory httpRequestFactoryInstance() {
+ return REQUEST_FACTORY;
+ }
+}
diff --git a/connectors/graphql/src/main/java/io/camunda/connector/graphql/model/GraphQLRequest.java b/connectors/graphql/src/main/java/io/camunda/connector/graphql/model/GraphQLRequest.java
new file mode 100644
index 0000000000..e4ad7e1dc9
--- /dev/null
+++ b/connectors/graphql/src/main/java/io/camunda/connector/graphql/model/GraphQLRequest.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH
+ * under one or more contributor license agreements. Licensed under a proprietary license.
+ * See the License.txt file for more information. You may not use this file
+ * except in compliance with the proprietary license.
+ */
+package io.camunda.connector.graphql.model;
+
+import io.camunda.connector.api.annotation.Secret;
+import io.camunda.connector.common.model.CommonRequest;
+import java.util.Objects;
+import javax.validation.constraints.NotBlank;
+
+public class GraphQLRequest extends CommonRequest {
+
+ @NotBlank @Secret private String query;
+ @Secret private Object variables;
+
+ public boolean hasQuery() {
+ return query != null;
+ }
+
+ public String getQuery() {
+ return query;
+ }
+
+ public void setQuery(String query) {
+ this.query = query;
+ }
+
+ public Object getVariables() {
+ return variables;
+ }
+
+ public void setVariables(Object variables) {
+ this.variables = variables;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ GraphQLRequest that = (GraphQLRequest) o;
+ return query.equals(that.query) && Objects.equals(variables, that.variables);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(query, variables);
+ }
+
+ @Override
+ public String toString() {
+ return "GraphQLRequest{" + "query='" + query + '\'' + ", variables=" + variables + '}';
+ }
+}
diff --git a/connectors/graphql/src/main/java/io/camunda/connector/graphql/model/GraphQLRequestWrapper.java b/connectors/graphql/src/main/java/io/camunda/connector/graphql/model/GraphQLRequestWrapper.java
new file mode 100644
index 0000000000..05d447ce02
--- /dev/null
+++ b/connectors/graphql/src/main/java/io/camunda/connector/graphql/model/GraphQLRequestWrapper.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH
+ * under one or more contributor license agreements. Licensed under a proprietary license.
+ * See the License.txt file for more information. You may not use this file
+ * except in compliance with the proprietary license.
+ */
+package io.camunda.connector.graphql.model;
+
+import io.camunda.connector.common.auth.Authentication;
+import java.util.Objects;
+import javax.validation.constraints.NotBlank;
+
+public class GraphQLRequestWrapper {
+ @NotBlank private GraphQLRequest graphql;
+ private Authentication authentication;
+
+ public GraphQLRequest getGraphql() {
+ return graphql;
+ }
+
+ public void setGraphql(GraphQLRequest graphql) {
+ this.graphql = graphql;
+ }
+
+ public Authentication getAuthentication() {
+ return authentication;
+ }
+
+ public void setAuthentication(Authentication authentication) {
+ this.authentication = authentication;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ GraphQLRequestWrapper that = (GraphQLRequestWrapper) o;
+ return graphql.equals(that.graphql) && Objects.equals(authentication, that.authentication);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(graphql, authentication);
+ }
+
+ @Override
+ public String toString() {
+ return "GraphQLRequestWrapper{"
+ + "graphql="
+ + graphql
+ + ", authentication="
+ + authentication
+ + '}';
+ }
+}
diff --git a/connectors/graphql/src/main/java/io/camunda/connector/graphql/model/GraphQLResult.java b/connectors/graphql/src/main/java/io/camunda/connector/graphql/model/GraphQLResult.java
new file mode 100644
index 0000000000..0d766d9646
--- /dev/null
+++ b/connectors/graphql/src/main/java/io/camunda/connector/graphql/model/GraphQLResult.java
@@ -0,0 +1,11 @@
+/*
+ * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH
+ * under one or more contributor license agreements. Licensed under a proprietary license.
+ * See the License.txt file for more information. You may not use this file
+ * except in compliance with the proprietary license.
+ */
+package io.camunda.connector.graphql.model;
+
+import io.camunda.connector.common.model.CommonResult;
+
+public class GraphQLResult extends CommonResult {}
diff --git a/connectors/graphql/src/main/java/io/camunda/connector/graphql/utils/GraphQLRequestMapper.java b/connectors/graphql/src/main/java/io/camunda/connector/graphql/utils/GraphQLRequestMapper.java
new file mode 100644
index 0000000000..65f167b20f
--- /dev/null
+++ b/connectors/graphql/src/main/java/io/camunda/connector/graphql/utils/GraphQLRequestMapper.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH
+ * under one or more contributor license agreements. Licensed under a proprietary license.
+ * See the License.txt file for more information. You may not use this file
+ * except in compliance with the proprietary license.
+ */
+package io.camunda.connector.graphql.utils;
+
+import io.camunda.connector.common.constants.Constants;
+import io.camunda.connector.common.model.CommonRequest;
+import io.camunda.connector.graphql.model.GraphQLRequest;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public final class GraphQLRequestMapper {
+
+ public static CommonRequest toCommonRequest(GraphQLRequest graphQLRequest) {
+ CommonRequest commonRequest = new CommonRequest();
+ final Map queryAndVariablesMap =
+ JsonSerializeHelper.queryAndVariablesToMap(graphQLRequest);
+ if (Constants.POST.equalsIgnoreCase(graphQLRequest.getMethod())) {
+ commonRequest.setBody(queryAndVariablesMap);
+ } else {
+ final Map queryAndVariablesStringMap =
+ queryAndVariablesMap.entrySet().stream()
+ .collect(Collectors.toMap(Map.Entry::getKey, e -> String.valueOf(e.getValue())));
+ commonRequest.setQueryParameters(queryAndVariablesStringMap);
+ }
+ commonRequest.setAuthentication(graphQLRequest.getAuthentication());
+ commonRequest.setMethod(graphQLRequest.getMethod());
+ commonRequest.setUrl(graphQLRequest.getUrl());
+ commonRequest.setConnectionTimeoutInSeconds(graphQLRequest.getConnectionTimeoutInSeconds());
+ return commonRequest;
+ }
+}
diff --git a/connectors/graphql/src/main/java/io/camunda/connector/graphql/utils/JsonSerializeHelper.java b/connectors/graphql/src/main/java/io/camunda/connector/graphql/utils/JsonSerializeHelper.java
new file mode 100644
index 0000000000..85877ace4b
--- /dev/null
+++ b/connectors/graphql/src/main/java/io/camunda/connector/graphql/utils/JsonSerializeHelper.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH
+ * under one or more contributor license agreements. Licensed under a proprietary license.
+ * See the License.txt file for more information. You may not use this file
+ * except in compliance with the proprietary license.
+ */
+package io.camunda.connector.graphql.utils;
+
+import com.google.gson.Gson;
+import io.camunda.connector.graphql.model.GraphQLRequest;
+import io.camunda.connector.graphql.model.GraphQLRequestWrapper;
+import java.util.HashMap;
+import java.util.Map;
+
+public final class JsonSerializeHelper {
+ public static GraphQLRequest serializeRequest(Gson gson, String input) {
+ GraphQLRequestWrapper graphQLRequestWrapper = gson.fromJson(input, GraphQLRequestWrapper.class);
+ GraphQLRequest graphQLRequest = graphQLRequestWrapper.getGraphql();
+ graphQLRequest.setAuthentication(graphQLRequestWrapper.getAuthentication());
+ return graphQLRequest;
+ }
+
+ public static Map queryAndVariablesToMap(GraphQLRequest graphQLRequest) {
+ final Map map = new HashMap<>();
+ map.put("query", getEscapedQuery(graphQLRequest));
+ if (graphQLRequest.getVariables() != null) {
+ map.put("variables", graphQLRequest.getVariables());
+ }
+ return map;
+ }
+
+ public static String getEscapedQuery(GraphQLRequest graphQLRequest) {
+ return graphQLRequest.getQuery().replace("\\n", "").replace("\\\"", "\"");
+ }
+}
diff --git a/connectors/graphql/src/main/resources/META-INF/services/io.camunda.connector.api.outbound.OutboundConnectorFunction b/connectors/graphql/src/main/resources/META-INF/services/io.camunda.connector.api.outbound.OutboundConnectorFunction
new file mode 100644
index 0000000000..3321632136
--- /dev/null
+++ b/connectors/graphql/src/main/resources/META-INF/services/io.camunda.connector.api.outbound.OutboundConnectorFunction
@@ -0,0 +1 @@
+io.camunda.connector.graphql.GraphQLFunction
\ No newline at end of file
diff --git a/connectors/graphql/src/test/java/io/camunda/connector/graphql/BaseTest.java b/connectors/graphql/src/test/java/io/camunda/connector/graphql/BaseTest.java
new file mode 100644
index 0000000000..34a391e999
--- /dev/null
+++ b/connectors/graphql/src/test/java/io/camunda/connector/graphql/BaseTest.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH
+ * under one or more contributor license agreements. Licensed under a proprietary license.
+ * See the License.txt file for more information. You may not use this file
+ * except in compliance with the proprietary license.
+ */
+package io.camunda.connector.graphql;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.nio.file.Files.readString;
+
+import com.google.api.client.json.gson.GsonFactory;
+import com.google.gson.Gson;
+import io.camunda.connector.graphql.components.GsonComponentSupplier;
+import io.camunda.connector.test.outbound.OutboundConnectorContextBuilder;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.stream.Stream;
+import org.junit.jupiter.params.provider.Arguments;
+
+public class BaseTest {
+
+ protected Gson gson = GsonComponentSupplier.gsonInstance();
+
+ protected GsonFactory gsonFactory = GsonComponentSupplier.gsonFactoryInstance();
+
+ protected interface SecretsConstant {
+ String URL = "URL_KEY";
+ String METHOD = "METHOD_KEY";
+ String CONNECT_TIMEOUT = "CONNECT_TIMEOUT_KEY";
+
+ interface Authentication {
+ String TOKEN = "TOKEN_KEY";
+ String PASSWORD = "PASSWORD_KEY";
+ String USERNAME = "USERNAME_KEY";
+ String OAUTH_TOKEN_ENDPOINT = "OAUTH_TOKEN_ENDPOINT_KEY";
+ String CLIENT_ID = "CLIENT_ID_KEY";
+ String CLIENT_SECRET = "CLIENT_SECRET_KEY";
+ String AUDIENCE = "AUDIENCE_KEY";
+ }
+
+ interface Variables {
+ String ID = "VARIABLE_ID";
+ }
+
+ interface Query {
+ String ID = "QUERY_ID";
+ String TEXT = "TEXT_KEY";
+ String TEXT_PART_1 = "TEXT_PART_1_KEY";
+ String TEXT_PART_2 = "TEXT_PART_2_KEY";
+ String TEXT_PART_3 = "TEXT_PART_3_KEY";
+ }
+ }
+
+ protected interface ActualValue {
+ String URL = "https://camunda.io/http-endpoint";
+ String METHOD = "GET";
+ String CONNECT_TIMEOUT = "50";
+
+ interface Authentication {
+ String TOKEN = "test token";
+ String PASSWORD = "1234567890";
+ String USERNAME = "test username";
+ String OAUTH_TOKEN_ENDPOINT = "https://test/api/v2/";
+ String CLIENT_ID = "bi1cekB123456GRWBBEgzdxA89S2T";
+ String CLIENT_SECRET = "Bzw6SL12345678934562eqg4fJM72EeeM2JQiF4BfbyYZUDCur7ntB";
+ String AUDIENCE = "https://test/api/v2/";
+ }
+
+ interface Variables {
+ String ID = "variableId";
+ String USER_AGENT = "http-connector-demo";
+ }
+
+ interface Query {
+ String ID = "secret id key";
+ String CUSTOMER_NAME_SECRET = "secret name";
+ String CUSTOMER_NAME_REAL = CUSTOMER_NAME_SECRET + " plus some text";
+ String CUSTOMER_EMAIL_SECRET = "start email plus secret email part plus end email";
+ String CUSTOMER_EMAIL_REAL = "start email plus " + CUSTOMER_EMAIL_SECRET + " plus end email";
+ String TEXT_PART_1 = "start secret text plus ";
+ String TEXT_PART_2 = "mid of text plus ";
+ String TEXT_PART_3 = "end of text";
+ String TEXT = TEXT_PART_1 + TEXT_PART_2 + TEXT_PART_3;
+ }
+ }
+
+ protected interface JsonKeys {
+ String CLUSTER_ID = "X-Camunda-Cluster-ID";
+ String USER_AGENT = "User-Agent";
+ String QUERY = "q";
+ String PRIORITY = "priority";
+ String CUSTOMER = "customer";
+ String ID = "id";
+ String NAME = "name";
+ String EMAIL = "email";
+ String TEXT = "text";
+ }
+
+ protected OutboundConnectorContextBuilder getContextBuilderWithSecrets() {
+ return OutboundConnectorContextBuilder.create()
+ .secret(SecretsConstant.URL, ActualValue.URL)
+ .secret(SecretsConstant.METHOD, ActualValue.METHOD)
+ .secret(SecretsConstant.CONNECT_TIMEOUT, ActualValue.CONNECT_TIMEOUT)
+ .secret(SecretsConstant.Authentication.TOKEN, ActualValue.Authentication.TOKEN)
+ .secret(SecretsConstant.Authentication.USERNAME, ActualValue.Authentication.USERNAME)
+ .secret(SecretsConstant.Authentication.PASSWORD, ActualValue.Authentication.PASSWORD)
+ .secret(
+ SecretsConstant.Authentication.OAUTH_TOKEN_ENDPOINT,
+ ActualValue.Authentication.OAUTH_TOKEN_ENDPOINT)
+ .secret(SecretsConstant.Authentication.CLIENT_ID, ActualValue.Authentication.CLIENT_ID)
+ .secret(
+ SecretsConstant.Authentication.CLIENT_SECRET, ActualValue.Authentication.CLIENT_SECRET)
+ .secret(SecretsConstant.Authentication.AUDIENCE, ActualValue.Authentication.AUDIENCE)
+ .secret(SecretsConstant.Variables.ID, ActualValue.Variables.ID)
+ .secret(SecretsConstant.Query.ID, ActualValue.Query.ID)
+ .secret(SecretsConstant.Query.TEXT, ActualValue.Query.TEXT)
+ .secret(SecretsConstant.Query.TEXT_PART_1, ActualValue.Query.TEXT_PART_1)
+ .secret(SecretsConstant.Query.TEXT_PART_2, ActualValue.Query.TEXT_PART_2)
+ .secret(SecretsConstant.Query.TEXT_PART_3, ActualValue.Query.TEXT_PART_3);
+ }
+
+ @SuppressWarnings("unchecked")
+ protected static Stream loadTestCasesFromResourceFile(final String fileWithTestCasesUri)
+ throws IOException {
+ final String cases = readString(new File(fileWithTestCasesUri).toPath(), UTF_8);
+ final Gson testingGson = GsonComponentSupplier.gsonInstance();
+ var array = testingGson.fromJson(cases, ArrayList.class);
+ return array.stream().map(testingGson::toJson).map(Arguments::of);
+ }
+}
diff --git a/connectors/graphql/src/test/java/io/camunda/connector/graphql/GraphQLFunctionInputValidationTest.java b/connectors/graphql/src/test/java/io/camunda/connector/graphql/GraphQLFunctionInputValidationTest.java
new file mode 100644
index 0000000000..99c35c80ea
--- /dev/null
+++ b/connectors/graphql/src/test/java/io/camunda/connector/graphql/GraphQLFunctionInputValidationTest.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH
+ * under one or more contributor license agreements. Licensed under a proprietary license.
+ * See the License.txt file for more information. You may not use this file
+ * except in compliance with the proprietary license.
+ */
+package io.camunda.connector.graphql;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import io.camunda.connector.api.outbound.OutboundConnectorContext;
+import io.camunda.connector.graphql.model.GraphQLRequest;
+import io.camunda.connector.graphql.utils.JsonSerializeHelper;
+import io.camunda.connector.impl.ConnectorInputException;
+import io.camunda.connector.test.outbound.OutboundConnectorContextBuilder;
+import java.io.IOException;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+public class GraphQLFunctionInputValidationTest extends BaseTest {
+
+ private static final String FAIL_REQUEST_CASES_PATH =
+ "src/test/resources/requests/fail-cases-request-without-one-required-field.json";
+
+ private static final String FAIL_CASES_TIMEOUT_CONNECTION_RESOURCE_PATH =
+ "src/test/resources/requests/fail-cases-connection-timeout-validation.json";
+
+ private static final String SUCCESS_CASES_TIMEOUT_CONNECTION_RESOURCE_PATH =
+ "src/test/resources/requests/success-cases-connection-timeout-validation.json";
+
+ private static final String REQUEST_METHOD_OBJECT_PLACEHOLDER =
+ "{\"graphql\":{\n \"method\": \"%s\",\n \"url\": \"https://camunda.io/http-endpoint\"\n}}";
+
+ private static final String REQUEST_ENDPOINT_OBJECT_PLACEHOLDER =
+ "{\"graphql\":{\n \"method\": \"get\",\n \"url\": \"%s\"\n}}";
+
+ private GraphQLFunction functionUnderTest;
+
+ @BeforeEach
+ void setup() {
+ functionUnderTest = new GraphQLFunction(null);
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"", " ", "\r\n"})
+ void shouldRaiseException_WhenExecuted_MethodMalformed(final String input) {
+ // Given
+ OutboundConnectorContext ctx =
+ OutboundConnectorContextBuilder.create()
+ .variables(String.format(REQUEST_METHOD_OBJECT_PLACEHOLDER, input))
+ .build();
+
+ // When
+ Throwable exception =
+ assertThrows(ConnectorInputException.class, () -> functionUnderTest.execute(ctx));
+
+ // Then
+ assertThat(exception.getMessage())
+ .contains("Found constraints violated while validating input", "method: must not be blank");
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"", " ", "iAmWrongUrl", "ftp://camunda.org/", "camunda@camunda.com"})
+ void shouldRaiseException_WhenExecuted_EndpointMalformed(final String input) {
+ // Given
+ OutboundConnectorContext ctx =
+ OutboundConnectorContextBuilder.create()
+ .variables(String.format(REQUEST_ENDPOINT_OBJECT_PLACEHOLDER, input))
+ .build();
+ // When
+ Throwable exception =
+ assertThrows(ConnectorInputException.class, () -> functionUnderTest.execute(ctx));
+ // Then
+ assertThat(exception.getMessage())
+ .contains(
+ "Found constraints violated while validating input",
+ "must match \"^(http://|https://|secrets).*$\"");
+ }
+
+ @ParameterizedTest(name = "Validate null field # {index}")
+ @MethodSource("failRequestCases")
+ void validate_shouldThrowExceptionWhenLeastOneNotExistRequestField(String input) {
+ // Given request without one required field
+ GraphQLRequest httpJsonRequest = JsonSerializeHelper.serializeRequest(gson, input);
+ OutboundConnectorContext context =
+ OutboundConnectorContextBuilder.create().variables(httpJsonRequest).build();
+ // When context.validate(request);
+ // Then expect exception that one required field not set
+ ConnectorInputException thrown =
+ assertThrows(
+ ConnectorInputException.class,
+ () -> context.validate(httpJsonRequest),
+ "ConnectorInputException was expected");
+ assertThat(thrown.getMessage()).contains("Found constraints violated while validating input");
+ }
+
+ @ParameterizedTest(name = "Validate connectionTimeout # {index}")
+ @MethodSource("failTimeOutConnectionCases")
+ void validate_shouldThrowExceptionConnectionTimeoutIsWrong(String input) {
+ // Given request without one required field
+ GraphQLRequest httpJsonRequest = JsonSerializeHelper.serializeRequest(gson, input);
+ OutboundConnectorContext context =
+ OutboundConnectorContextBuilder.create().variables(httpJsonRequest).build();
+ // When context.validate(request);
+ // Then expect exception
+ ConnectorInputException thrown =
+ assertThrows(
+ ConnectorInputException.class,
+ () -> context.validate(httpJsonRequest),
+ "ConnectorInputException was expected");
+ assertThat(thrown.getMessage()).contains("Found constraints violated while validating input");
+ }
+
+ @ParameterizedTest(name = "Success validate connectionTimeout # {index}")
+ @MethodSource("successTimeOutConnectionCases")
+ void validate_shouldValidateWithoutException(String input) {
+ // Given request without one required field
+ GraphQLRequest httpJsonRequest = JsonSerializeHelper.serializeRequest(gson, input);
+ OutboundConnectorContext context =
+ OutboundConnectorContextBuilder.create().variables(httpJsonRequest).build();
+ // When context.validate(request);
+ // Then expect normal validate without exception
+ context.validate(httpJsonRequest);
+ }
+
+ protected static Stream failRequestCases() throws IOException {
+ return loadTestCasesFromResourceFile(FAIL_REQUEST_CASES_PATH);
+ }
+
+ private static Stream failTimeOutConnectionCases() throws IOException {
+ return loadTestCasesFromResourceFile(FAIL_CASES_TIMEOUT_CONNECTION_RESOURCE_PATH);
+ }
+
+ private static Stream successTimeOutConnectionCases() throws IOException {
+ return loadTestCasesFromResourceFile(SUCCESS_CASES_TIMEOUT_CONNECTION_RESOURCE_PATH);
+ }
+}
diff --git a/connectors/graphql/src/test/java/io/camunda/connector/graphql/GraphQLFunctionSecretsTest.java b/connectors/graphql/src/test/java/io/camunda/connector/graphql/GraphQLFunctionSecretsTest.java
new file mode 100644
index 0000000000..b0f62f31e2
--- /dev/null
+++ b/connectors/graphql/src/test/java/io/camunda/connector/graphql/GraphQLFunctionSecretsTest.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH
+ * under one or more contributor license agreements. Licensed under a proprietary license.
+ * See the License.txt file for more information. You may not use this file
+ * except in compliance with the proprietary license.
+ */
+package io.camunda.connector.graphql;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.fail;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.gson.JsonObject;
+import io.camunda.connector.api.outbound.OutboundConnectorContext;
+import io.camunda.connector.common.auth.Authentication;
+import io.camunda.connector.common.auth.BasicAuthentication;
+import io.camunda.connector.common.auth.BearerAuthentication;
+import io.camunda.connector.common.auth.NoAuthentication;
+import io.camunda.connector.common.auth.OAuthAuthentication;
+import io.camunda.connector.graphql.model.GraphQLRequest;
+import io.camunda.connector.graphql.utils.JsonSerializeHelper;
+import java.io.IOException;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+public class GraphQLFunctionSecretsTest extends BaseTest {
+ private static final String SUCCESS_REPLACE_SECRETS_CASES_PATH =
+ "src/test/resources/requests/success-cases-replace-secrets.json";
+
+ private OutboundConnectorContext context;
+
+ protected static Stream successReplaceSecretsCases() throws IOException {
+ return loadTestCasesFromResourceFile(SUCCESS_REPLACE_SECRETS_CASES_PATH);
+ }
+
+ @ParameterizedTest(name = "Should replace request secrets")
+ @MethodSource("successReplaceSecretsCases")
+ void replaceSecrets_shouldReplaceRequestSecrets(String input) {
+ // Given request with secrets
+ GraphQLRequest graphQLRequest = JsonSerializeHelper.serializeRequest(gson, input);
+ context = getContextBuilderWithSecrets().variables(graphQLRequest).build();
+ // When
+ context.replaceSecrets(graphQLRequest);
+ // Then should replace secrets
+ assertThat(graphQLRequest.getUrl()).isEqualTo(ActualValue.URL);
+ assertThat(graphQLRequest.getMethod()).isEqualTo(ActualValue.METHOD);
+ assertThat(graphQLRequest.getConnectionTimeoutInSeconds())
+ .isEqualTo(ActualValue.CONNECT_TIMEOUT);
+ }
+
+ @ParameterizedTest(name = "Should replace auth secrets")
+ @MethodSource("successReplaceSecretsCases")
+ void replaceSecrets_shouldReplaceAuthSecrets(String input) {
+ // Given request with secrets
+ GraphQLRequest graphQLRequest = JsonSerializeHelper.serializeRequest(gson, input);
+ context = getContextBuilderWithSecrets().variables(graphQLRequest).build();
+ // When
+ context.replaceSecrets(graphQLRequest);
+ // Then should replace secrets
+ Authentication authentication = graphQLRequest.getAuthentication();
+ if (authentication instanceof NoAuthentication) {
+ // nothing check in this case
+ } else if (authentication instanceof BearerAuthentication) {
+ BearerAuthentication bearerAuth = (BearerAuthentication) authentication;
+ assertThat(bearerAuth.getToken()).isEqualTo(ActualValue.Authentication.TOKEN);
+ } else if (authentication instanceof BasicAuthentication) {
+ BasicAuthentication basicAuth = (BasicAuthentication) authentication;
+ assertThat(basicAuth.getPassword()).isEqualTo(ActualValue.Authentication.PASSWORD);
+ assertThat(basicAuth.getUsername()).isEqualTo(ActualValue.Authentication.USERNAME);
+ } else if (authentication instanceof OAuthAuthentication) {
+ OAuthAuthentication oAuthAuthentication = (OAuthAuthentication) authentication;
+ assertThat(oAuthAuthentication.getOauthTokenEndpoint())
+ .isEqualTo(ActualValue.Authentication.OAUTH_TOKEN_ENDPOINT);
+ assertThat(oAuthAuthentication.getClientId()).isEqualTo(ActualValue.Authentication.CLIENT_ID);
+ assertThat(oAuthAuthentication.getClientSecret())
+ .isEqualTo(ActualValue.Authentication.CLIENT_SECRET);
+ assertThat(oAuthAuthentication.getAudience()).isEqualTo(ActualValue.Authentication.AUDIENCE);
+ } else {
+ fail("unknown authentication type");
+ }
+ }
+
+ @ParameterizedTest(name = "Should replace variables secrets")
+ @MethodSource("successReplaceSecretsCases")
+ void replaceSecrets_shouldReplaceVariablesSecrets(String input) {
+ // Given request with secrets
+ // GraphQLRequestWrapper graphQLRequest = gson.fromJson(input, GraphQLRequestWrapper.class);
+ GraphQLRequest graphQLRequest = JsonSerializeHelper.serializeRequest(gson, input);
+ context = getContextBuilderWithSecrets().variables(graphQLRequest).build();
+ // When
+ context.replaceSecrets(graphQLRequest);
+ // Then should replace secrets
+ JsonObject variables = gson.toJsonTree(graphQLRequest.getVariables()).getAsJsonObject();
+
+ assertThat(variables.get(JsonKeys.ID).getAsString()).isEqualTo(ActualValue.Variables.ID);
+ }
+
+ @ParameterizedTest(name = "Should replace query secrets")
+ @MethodSource("successReplaceSecretsCases")
+ void replaceSecrets_shouldReplaceQuerySecrets(String input) {
+ // Given request with secrets
+ GraphQLRequest graphQLRequest = JsonSerializeHelper.serializeRequest(gson, input);
+ context = getContextBuilderWithSecrets().variables(graphQLRequest).build();
+ // When
+ context.replaceSecrets(graphQLRequest);
+ // Then should replace secrets
+ String query = graphQLRequest.getQuery();
+ assertFalse(query.contains("{{secrets.QUERY_ID}}"));
+ assertTrue(query.contains(ActualValue.Query.ID));
+ }
+
+ @Test
+ void replaceSecrets_shouldReplaceQueryWhenQueryIsString() {
+ // Given request with secrets
+ GraphQLRequest request = new GraphQLRequest();
+ request.setQuery(
+ "{{secrets."
+ + SecretsConstant.Query.TEXT_PART_1
+ + "}}"
+ + "{{secrets."
+ + SecretsConstant.Query.TEXT_PART_2
+ + "}}"
+ + "{{secrets."
+ + SecretsConstant.Query.TEXT_PART_3
+ + "}}");
+ context = getContextBuilderWithSecrets().variables(request).build();
+ // When
+ context.replaceSecrets(request);
+ // Then should replace secrets
+ assertThat(request.getQuery().toString()).isEqualTo(ActualValue.Query.TEXT);
+ }
+}
diff --git a/connectors/graphql/src/test/java/io/camunda/connector/graphql/GraphQLFunctionTest.java b/connectors/graphql/src/test/java/io/camunda/connector/graphql/GraphQLFunctionTest.java
new file mode 100644
index 0000000000..babc6f3da6
--- /dev/null
+++ b/connectors/graphql/src/test/java/io/camunda/connector/graphql/GraphQLFunctionTest.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH
+ * under one or more contributor license agreements. Licensed under a proprietary license.
+ * See the License.txt file for more information. You may not use this file
+ * except in compliance with the proprietary license.
+ */
+package io.camunda.connector.graphql;
+
+import static org.apache.http.entity.ContentType.APPLICATION_JSON;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.catchException;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.google.api.client.http.GenericUrl;
+import com.google.api.client.http.HttpContent;
+import com.google.api.client.http.HttpHeaders;
+import com.google.api.client.http.HttpRequest;
+import com.google.api.client.http.HttpRequestFactory;
+import com.google.api.client.http.HttpResponse;
+import com.google.api.client.http.HttpResponseException;
+import com.google.gson.JsonObject;
+import io.camunda.connector.api.error.ConnectorException;
+import io.camunda.connector.graphql.model.GraphQLRequest;
+import io.camunda.connector.graphql.model.GraphQLResult;
+import io.camunda.connector.impl.ConnectorInputException;
+import io.camunda.connector.test.outbound.OutboundConnectorContextBuilder;
+import java.io.IOException;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+public class GraphQLFunctionTest extends BaseTest {
+
+ private static final String SUCCESS_CASES_RESOURCE_PATH =
+ "src/test/resources/requests/success-test-cases.json";
+
+ private static final String SUCCESS_CASES_OAUTH_RESOURCE_PATH =
+ "src/test/resources/requests/success-test-cases-oauth.json";
+ private static final String FAIL_CASES_RESOURCE_PATH =
+ "src/test/resources/requests/fail-test-cases.json";
+
+ @Mock private HttpRequestFactory requestFactory;
+ @Mock private HttpRequest httpRequest;
+ @Mock private HttpResponse httpResponse;
+
+ private GraphQLFunction functionUnderTest;
+
+ @BeforeEach
+ public void setup() {
+ functionUnderTest = new GraphQLFunction(gson, requestFactory, gsonFactory, null);
+ }
+
+ @ParameterizedTest(name = "Executing test case: {0}")
+ @MethodSource("successCases")
+ void shouldReturnResult_WhenExecuted(final String input)
+ throws IOException, InstantiationException, IllegalAccessException {
+ // given - minimal required entity
+ Object functionCallResponseAsObject = arrange(input);
+
+ // then
+ verify(httpRequest).execute();
+ assertThat(functionCallResponseAsObject).isInstanceOf(GraphQLResult.class);
+ assertThat(((GraphQLResult) functionCallResponseAsObject).getHeaders())
+ .containsValue(APPLICATION_JSON.getMimeType());
+ }
+
+ @ParameterizedTest(name = "Executing test case: {0}")
+ @MethodSource("successCasesOauth")
+ void shouldReturnResultOAuth_WhenExecuted(final String input)
+ throws IOException, InstantiationException, IllegalAccessException {
+ Object functionCallResponseAsObject = arrange(input);
+
+ // then
+ verify(httpRequest, times(2)).execute();
+ assertThat(functionCallResponseAsObject).isInstanceOf(GraphQLResult.class);
+ assertThat(((GraphQLResult) functionCallResponseAsObject).getHeaders())
+ .containsValue(APPLICATION_JSON.getMimeType());
+ }
+
+ private Object arrange(String input)
+ throws IOException, InstantiationException, IllegalAccessException {
+ final var context =
+ OutboundConnectorContextBuilder.create().variables(input).secrets(name -> "foo").build();
+ when(requestFactory.buildRequest(
+ anyString(), any(GenericUrl.class), nullable(HttpContent.class)))
+ .thenReturn(httpRequest);
+ when(httpResponse.getHeaders())
+ .thenReturn(new HttpHeaders().setContentType(APPLICATION_JSON.getMimeType()));
+ when(httpRequest.execute()).thenReturn(httpResponse);
+
+ // when
+ return functionUnderTest.execute(context);
+ }
+
+ @ParameterizedTest(name = "Executing test case: {0}")
+ @MethodSource("failCases")
+ void shouldReturnFallbackResult_WhenMalformedRequest(final String input) {
+ final var context =
+ OutboundConnectorContextBuilder.create().variables(input).secrets(name -> "foo").build();
+
+ // when
+ var exceptionThrown = catchException(() -> functionUnderTest.execute(context));
+
+ // then
+ assertThat(exceptionThrown)
+ .isInstanceOf(ConnectorInputException.class)
+ .hasMessageContaining("ValidationException");
+ }
+
+ @ParameterizedTest(name = "Executing test case: {0}")
+ @MethodSource("successCases")
+ void execute_shouldSetConnectTime(final String input)
+ throws IOException, InstantiationException, IllegalAccessException {
+ // given - minimal required entity
+ final var context =
+ OutboundConnectorContextBuilder.create().variables(input).secrets(name -> "foo").build();
+ final var expectedTimeInMilliseconds =
+ Integer.parseInt(
+ gson.fromJson(
+ gson.fromJson(input, JsonObject.class).get("graphql").toString(),
+ GraphQLRequest.class)
+ .getConnectionTimeoutInSeconds())
+ * 1000;
+
+ when(requestFactory.buildRequest(
+ anyString(), any(GenericUrl.class), nullable(HttpContent.class)))
+ .thenReturn(httpRequest);
+ when(httpResponse.getHeaders())
+ .thenReturn(new HttpHeaders().setContentType(APPLICATION_JSON.getMimeType()));
+ when(httpRequest.execute()).thenReturn(httpResponse);
+ // when
+ functionUnderTest.execute(context);
+ // then
+ verify(httpRequest).setConnectTimeout(expectedTimeInMilliseconds);
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = {400, 404, 500})
+ void execute_shouldPassOnHttpErrorAsErrorCode(final int input) throws IOException {
+ // given
+ final var request =
+ "{\"graphql\": { \"method\": \"get\", \"url\": \"https://camunda.io/http-endpoint\", \"query\": \"testQuery\"}, \"authentication\": { \"type\": \"noAuth\" } }";
+ final var context = OutboundConnectorContextBuilder.create().variables(request).build();
+
+ when(requestFactory.buildRequest(
+ anyString(), any(GenericUrl.class), nullable(HttpContent.class)))
+ .thenReturn(httpRequest);
+ when(httpResponse.getHeaders())
+ .thenReturn(new HttpHeaders().setContentType(APPLICATION_JSON.getMimeType()));
+ when(httpResponse.getStatusCode()).thenReturn(input);
+ when(httpResponse.parseAsString()).thenReturn("message");
+ doThrow(new HttpResponseException(httpResponse)).when(httpRequest).execute();
+ // when
+ final var result = catchException(() -> functionUnderTest.execute(context));
+ // then HTTP status code is passed on as error code
+ assertThat(result)
+ .isInstanceOf(ConnectorException.class)
+ .extracting("errorCode")
+ .isEqualTo(String.valueOf(input));
+ }
+
+ @Test
+ void execute_shouldNotUseErrorDataOnHttpError() throws IOException {
+ // given
+ final var request =
+ "{\"graphql\": {\"method\": \"get\", \"url\": \"https://camunda.io/http-endpoint\", \"query\": \"testQuery\"}, \"authentication\": { \"type\": \"noAuth\" } }";
+ final var context = OutboundConnectorContextBuilder.create().variables(request).build();
+ final var httpException = mock(HttpResponseException.class);
+
+ when(requestFactory.buildRequest(
+ anyString(), any(GenericUrl.class), nullable(HttpContent.class)))
+ .thenReturn(httpRequest);
+ when(httpException.getStatusCode()).thenReturn(500);
+ when(httpException.getMessage()).thenReturn("message");
+ doThrow(httpException).when(httpRequest).execute();
+ // when
+ final var result = catchException(() -> functionUnderTest.execute(context));
+ // then HTTP status code is passed on as error code
+ verify(httpException, times(0)).getContent();
+ assertThat(result)
+ .isInstanceOf(ConnectorException.class)
+ .hasMessage("message")
+ .extracting("errorCode")
+ .isEqualTo("500");
+ }
+
+ private static Stream successCases() throws IOException {
+ return loadTestCasesFromResourceFile(SUCCESS_CASES_RESOURCE_PATH);
+ }
+
+ private static Stream successCasesOauth() throws IOException {
+ return loadTestCasesFromResourceFile(SUCCESS_CASES_OAUTH_RESOURCE_PATH);
+ }
+
+ private static Stream failCases() throws IOException {
+ return loadTestCasesFromResourceFile(FAIL_CASES_RESOURCE_PATH);
+ }
+}
diff --git a/connectors/graphql/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/connectors/graphql/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..ca6ee9cea8
--- /dev/null
+++ b/connectors/graphql/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
@@ -0,0 +1 @@
+mock-maker-inline
\ No newline at end of file
diff --git a/connectors/graphql/src/test/resources/requests/fail-cases-connection-timeout-validation.json b/connectors/graphql/src/test/resources/requests/fail-cases-connection-timeout-validation.json
new file mode 100644
index 0000000000..9098aecca7
--- /dev/null
+++ b/connectors/graphql/src/test/resources/requests/fail-cases-connection-timeout-validation.json
@@ -0,0 +1,103 @@
+[
+ {
+ "descriptionOfTest": "Negative value",
+ "graphql": {
+ "method": "get",
+ "url": "https://camunda.io/http-endpoint",
+ "connectionTimeoutInSeconds": "-1",
+ "authentication": {
+ "type": "noAuth"
+ }
+ }
+ },
+ {
+ "descriptionOfTest": "Negative value",
+ "graphql": {
+ "method": "get",
+ "url": "https://camunda.io/http-endpoint",
+ "connectionTimeoutInSeconds": "-99",
+ "authentication": {
+ "type": "noAuth"
+ }
+ }
+ },
+ {
+ "descriptionOfTest": "String value",
+ "graphql": {
+ "method": "get",
+ "url": "https://camunda.io/http-endpoint",
+ "connectionTimeoutInSeconds": "bad value",
+ "authentication": {
+ "type": "noAuth"
+ }
+ }
+ },
+ {
+ "descriptionOfTest": "Normal value with space",
+ "graphql": {
+ "method": "get",
+ "url": "https://camunda.io/http-endpoint",
+ "connectionTimeoutInSeconds": "23 ",
+ "authentication": {
+ "type": "noAuth"
+ }
+ }
+ },
+ {
+ "descriptionOfTest": "Blank value",
+ "graphql": {
+ "method": "get",
+ "url": "https://camunda.io/http-endpoint",
+ "connectionTimeoutInSeconds": " ",
+ "authentication": {
+ "type": "noAuth"
+ }
+ }
+ },
+ {
+ "descriptionOfTest": "Float value",
+ "graphql": {
+ "method": "get",
+ "url": "https://camunda.io/http-endpoint",
+ "connectionTimeoutInSeconds": "1.56",
+ "authentication": {
+ "type": "bearer",
+ "token": "secrets.TOKEN_KEY"
+ }
+ }
+ },
+ {
+ "descriptionOfTest": "Float value",
+ "graphql": {
+ "method": "get",
+ "url": "https://camunda.io/http-endpoint",
+ "connectionTimeoutInSeconds": "0.1",
+ "authentication": {
+ "type": "noAuth"
+ }
+ }
+ },
+ {
+ "descriptionOfTest": "Binary value",
+ "graphql": {
+ "method": "get",
+ "url": "https://camunda.io/http-endpoint",
+ "connectionTimeoutInSeconds": "0xFF",
+ "authentication": {
+ "type": "noAuth"
+ }
+ }
+ },
+ {
+ "descriptionOfTest": "Binary value",
+ "graphql": {
+ "method": "get",
+ "url": "https://camunda.io/http-endpoint",
+ "connectionTimeoutInSeconds": "0b0011",
+ "authentication": {
+ "type": "bearer",
+ "token": "secrets.TOKEN_KEY"
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/connectors/graphql/src/test/resources/requests/fail-cases-request-without-one-required-field.json b/connectors/graphql/src/test/resources/requests/fail-cases-request-without-one-required-field.json
new file mode 100644
index 0000000000..e5770cb394
--- /dev/null
+++ b/connectors/graphql/src/test/resources/requests/fail-cases-request-without-one-required-field.json
@@ -0,0 +1,178 @@
+[
+ {
+ "testDescription": "without method field",
+ "authentication": {
+ "type": "noAuth"
+ },
+ "graphql": {
+ "url": "testmail@testmail.com",
+ "query": "query Root($id: ID) {\n person (id: $id) {\n id\n name\n }\n}",
+ "variables": {
+ "id": "cGVvcGxlOjI="
+ }
+ }
+ },
+ {
+ "testDescription": "without URL field",
+ "authentication": {
+ "type": "noAuth"
+ },
+ "graphql": {
+ "method": "get",
+ "query": "query Root($id: ID) {\n person (id: $id) {\n id\n name\n }\n}",
+ "variables": {
+ "id": "cGVvcGxlOjI="
+ }
+ }
+ },
+ {
+ "testDescription": "bearer auth type without URL field",
+ "authentication": {
+ "type": "bearer"
+ },
+ "graphql": {
+ "method": "get",
+ "query": "query Root($id: ID) {\n person (id: $id) {\n id\n name\n }\n}",
+ "variables": {
+ "id": "cGVvcGxlOjI="
+ }
+ }
+ },
+ {
+ "testDescription": "basic auth type without URL field",
+ "authentication": {
+ "type": "basic"
+ },
+ "graphql": {
+ "method": "get",
+ "query": "query Root($id: ID) {\n person (id: $id) {\n id\n name\n }\n}",
+ "variables": {
+ "id": "cGVvcGxlOjI="
+ }
+ }
+ },
+ {
+ "testDescription": "bearer auth without token",
+ "authentication": {
+ "type": "bearer"
+ },
+ "graphql": {
+ "method": "get",
+ "url": "testmail@testmail.com",
+ "query": "query Root($id: ID) {\n person (id: $id) {\n id\n name\n }\n}",
+ "variables": {
+ "id": "cGVvcGxlOjI="
+ }
+ }
+ },
+ {
+ "testDescription": "basic auth without password",
+ "authentication": {
+ "type": "basic",
+ "username": "username"
+ },
+ "graphql": {
+ "method": "get",
+ "url": "testmail@testmail.com",
+ "query": "query Root($id: ID) {\n person (id: $id) {\n id\n name\n }\n}",
+ "variables": {
+ "id": "cGVvcGxlOjI="
+ }
+ }
+ },
+ {
+ "testDescription": "basic auth without username",
+ "authentication": {
+ "type": "basic",
+ "password": "password"
+ },
+ "graphql": {
+ "method": "get",
+ "url": "testmail@testmail.com",
+ "query": "query Root($id: ID) {\n person (id: $id) {\n id\n name\n }\n}",
+ "variables": {
+ "id": "cGVvcGxlOjI="
+ }
+ }
+ },
+ {
+ "descriptionOfTest": "oauth request without client id",
+ "authentication":{
+ "oauthTokenEndpoint":"https://abc.eu.auth0.com/api/v2/",
+ "scopes": "read:clients",
+ "audience":"https://abc.eu.auth0.com/api/v2/",
+ "clientSecret":"secrets.CLIENT_SECRET",
+ "type":"oauth-client-credentials-flow",
+ "clientAuthentication":"basicAuthHeader"
+ },
+ "graphql": {
+ "method": "post",
+ "url": "https://abc/def",
+ "connectionTimeoutInSeconds": "30",
+ "query": "query Root($id: ID) {\n person (id: $id) {\n id\n name\n }\n}",
+ "variables": {
+ "id": "cGVvcGxlOjI="
+ }
+ }
+ },
+ {
+ "descriptionOfTest": "oauth request without client secret",
+ "authentication":{
+ "oauthTokenEndpoint":"https://abc.eu.auth0.com/api/v2/",
+ "scopes": "read:clients",
+ "audience":"https://abc.eu.auth0.com/api/v2/",
+ "clientId":"secrets.CLIENT_ID",
+ "type":"oauth-client-credentials-flow",
+ "clientAuthentication":"basicAuthHeader"
+ },
+ "graphql": {
+ "method": "post",
+ "url": "https://abc/def",
+ "connectionTimeoutInSeconds": "30",
+ "query": "query Root($id: ID) {\n person (id: $id) {\n id\n name\n }\n}",
+ "variables": {
+ "id": "cGVvcGxlOjI="
+ }
+ }
+ },
+ {
+ "descriptionOfTest": "oauth request without oauth token endpoint",
+ "authentication":{
+ "scopes": "read:clients",
+ "audience":"https://abc.eu.auth0.com/api/v2/",
+ "clientId":"secrets.CLIENT_ID",
+ "clientSecret":"secrets.CLIENT_SECRET",
+ "type":"oauth-client-credentials-flow",
+ "clientAuthentication":"basicAuthHeader"
+ },
+ "graphql": {
+ "method": "post",
+ "url": "https://abc/def",
+ "connectionTimeoutInSeconds": "30",
+ "query": "query Root($id: ID) {\n person (id: $id) {\n id\n name\n }\n}",
+ "variables": {
+ "id": "cGVvcGxlOjI="
+ }
+ }
+ },
+ {
+ "descriptionOfTest": "oauth request without client authentication",
+ "authentication":{
+ "oauthTokenEndpoint":"https://abc.eu.auth0.com/api/v2/",
+ "scopes": "read:clients",
+ "audience":"https://abc.eu.auth0.com/api/v2/",
+ "clientId":"secrets.CLIENT_ID",
+ "clientSecret":"secrets.CLIENT_SECRET",
+ "type":"oauth-client-credentials-flow"
+ },
+ "graphql": {
+ "method": "post",
+ "url": "https://abc/def",
+ "connectionTimeoutInSeconds": "30",
+ "query": "query Root($id: ID) {\n person (id: $id) {\n id\n name\n }\n}",
+ "variables": {
+ "id": "cGVvcGxlOjI="
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/connectors/graphql/src/test/resources/requests/fail-test-cases.json b/connectors/graphql/src/test/resources/requests/fail-test-cases.json
new file mode 100644
index 0000000000..4497488fc2
--- /dev/null
+++ b/connectors/graphql/src/test/resources/requests/fail-test-cases.json
@@ -0,0 +1,33 @@
+[
+ {
+ "descriptionOfTest": "No method",
+ "graphql": {
+ "url": "https://camunda.io/http-endpoint",
+ "query": "query Root($id: ID) {\n person (id: $id) {\n id\n name\n }\n}",
+ "variables": {
+ "id": "cGVvcGxlOjI="
+ }
+ }
+ },
+ {
+ "descriptionOfTest": "No URL",
+ "graphql": {
+ "method": "get",
+ "query": "query Root($id: ID) {\n person (id: $id) {\n id\n name\n }\n}",
+ "variables": {
+ "id": "cGVvcGxlOjI="
+ }
+ }
+ },
+ {
+ "descriptionOfTest": "Malformed URL",
+ "graphql": {
+ "method": "get",
+ "url": "testmail@testmail.com",
+ "query": "query Root($id: ID) {\n person (id: $id) {\n id\n name\n }\n}",
+ "variables": {
+ "id": "cGVvcGxlOjI="
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/connectors/graphql/src/test/resources/requests/success-cases-connection-timeout-validation.json b/connectors/graphql/src/test/resources/requests/success-cases-connection-timeout-validation.json
new file mode 100644
index 0000000000..5a839b3d3a
--- /dev/null
+++ b/connectors/graphql/src/test/resources/requests/success-cases-connection-timeout-validation.json
@@ -0,0 +1,53 @@
+[
+ {
+ "descriptionOfTest": "0 value",
+ "graphql": {
+ "method": "get",
+ "url": "https://camunda.io/http-endpoint",
+ "query": "query Root($id: ID) {\n person (id: $id) {\n id\n name\n }\n}",
+ "connectionTimeoutInSeconds": "0"
+ },
+ "authentication": {
+ "type": "noAuth"
+ }
+ },
+ {
+ "descriptionOfTest": "0 value with bearer auth",
+ "graphql": {
+ "method": "get",
+ "url": "https://camunda.io/http-endpoint",
+ "query": "query Root($id: ID) {\n person (id: $id) {\n id\n name\n }\n}",
+ "connectionTimeoutInSeconds": "0"
+ },
+ "authentication": {
+ "type": "bearer",
+ "token": "secrets.TOKEN_KEY"
+ }
+ },
+ {
+ "descriptionOfTest": "positive value",
+ "graphql": {
+ "method": "get",
+ "url": "https://camunda.io/http-endpoint",
+ "query": "query Root($id: ID) {\n person (id: $id) {\n id\n name\n }\n}",
+ "connectionTimeoutInSeconds": "99"
+ },
+ "authentication": {
+ "type": "bearer",
+ "token": "secrets.TOKEN_KEY"
+ }
+ },
+ {
+ "descriptionOfTest": "secrets value",
+ "graphql": {
+ "method": "get",
+ "url": "https://camunda.io/http-endpoint",
+ "query": "query Root($id: ID) {\n person (id: $id) {\n id\n name\n }\n}",
+ "connectionTimeoutInSeconds": "secrets.SOME_SECRET_KEY"
+ },
+ "authentication": {
+ "type": "bearer",
+ "token": "secrets.TOKEN_KEY"
+ }
+ }
+]
\ No newline at end of file
diff --git a/connectors/graphql/src/test/resources/requests/success-cases-replace-secrets.json b/connectors/graphql/src/test/resources/requests/success-cases-replace-secrets.json
new file mode 100644
index 0000000000..901ae3db00
--- /dev/null
+++ b/connectors/graphql/src/test/resources/requests/success-cases-replace-secrets.json
@@ -0,0 +1,67 @@
+[
+ {
+ "graphql": {
+ "url": "secrets.URL_KEY",
+ "method": "secrets.METHOD_KEY",
+ "connectionTimeoutInSeconds":"secrets.CONNECT_TIMEOUT_KEY",
+ "query": "query Root($id: {{secrets.QUERY_ID}}) {\n person (id: $id) {\n id\n name\n }\n}",
+ "variables": {
+ "id": "{{secrets.VARIABLE_ID}}"
+ }
+ },
+ "authentication": {
+ "type": "noAuth"
+ }
+ },
+ {
+ "graphql": {
+ "url": "secrets.URL_KEY",
+ "method": "secrets.METHOD_KEY",
+ "connectionTimeoutInSeconds":"secrets.CONNECT_TIMEOUT_KEY",
+ "query": "query Root($id: {{secrets.QUERY_ID}}) {\n person (id: $id) {\n id\n name\n }\n}",
+ "variables": {
+ "id": "{{secrets.VARIABLE_ID}}"
+ }
+ },
+ "authentication": {
+ "type": "bearer",
+ "token": "secrets.TOKEN_KEY"
+ }
+ },
+ {
+ "graphql": {
+ "url": "secrets.URL_KEY",
+ "method": "secrets.METHOD_KEY",
+ "connectionTimeoutInSeconds":"secrets.CONNECT_TIMEOUT_KEY",
+ "query": "query Root($id: {{secrets.QUERY_ID}}) {\n person (id: $id) {\n id\n name\n }\n}",
+ "variables": {
+ "id": "{{secrets.VARIABLE_ID}}"
+ }
+ },
+ "authentication": {
+ "type": "basic",
+ "password": "secrets.PASSWORD_KEY",
+ "username": "secrets.USERNAME_KEY"
+ }
+ },
+ {
+ "graphql": {
+ "url": "secrets.URL_KEY",
+ "method": "secrets.METHOD_KEY",
+ "connectionTimeoutInSeconds":"secrets.CONNECT_TIMEOUT_KEY",
+ "query": "query Root($id: {{secrets.QUERY_ID}}) {\n person (id: $id) {\n id\n name\n }\n}",
+ "variables": {
+ "id": "{{secrets.VARIABLE_ID}}"
+ }
+ },
+ "authentication": {
+ "oauthTokenEndpoint":"secrets.OAUTH_TOKEN_ENDPOINT_KEY",
+ "scopes": "test",
+ "audience":"secrets.AUDIENCE_KEY",
+ "clientId":"secrets.CLIENT_ID_KEY",
+ "clientSecret":"secrets.CLIENT_SECRET_KEY",
+ "type": "oauth-client-credentials-flow",
+ "clientAuthentication":"secrets.CLIENT_AUTHENTICATION_KEY"
+ }
+ }
+]
\ No newline at end of file
diff --git a/connectors/graphql/src/test/resources/requests/success-test-cases-oauth.json b/connectors/graphql/src/test/resources/requests/success-test-cases-oauth.json
new file mode 100644
index 0000000000..8286e07c5d
--- /dev/null
+++ b/connectors/graphql/src/test/resources/requests/success-test-cases-oauth.json
@@ -0,0 +1,22 @@
+[{
+ "descriptionOfTest": "Normal request with oauth",
+ "authentication":{
+ "oauthTokenEndpoint":"https://dev-test.eu.auth0.com/api/v2/",
+ "scopes": "read:clients",
+ "audience":"https://dev-test.eu.auth0.com/api/v2/",
+ "clientId":"secrets.CLIENT_ID",
+ "clientSecret":"secrets.CLIENT_SECRET",
+ "type":"oauth-client-credentials-flow",
+ "clientAuthentication":"basicAuthHeader"
+ },
+ "graphql": {
+ "method": "post",
+ "url": "https://camunda.io/http-endpoint",
+ "connectionTimeoutInSeconds": "30",
+ "query": "query Root($id: ID) {\n person (id: $id) {\n id\n name\n }\n}",
+ "variables": {
+ "id": "cGVvcGxlOjI="
+ }
+ }
+}
+]
\ No newline at end of file
diff --git a/connectors/graphql/src/test/resources/requests/success-test-cases.json b/connectors/graphql/src/test/resources/requests/success-test-cases.json
new file mode 100644
index 0000000000..23f95d02fe
--- /dev/null
+++ b/connectors/graphql/src/test/resources/requests/success-test-cases.json
@@ -0,0 +1,88 @@
+[
+ {
+ "descriptionOfTest": "Normal request with no auth",
+ "authentication": {
+ "type": "noAuth"
+ },
+ "graphql": {
+ "method": "get",
+ "url": "https://camunda.io/http-endpoint",
+ "connectionTimeoutInSeconds": "20",
+ "query": "query Root($id: ID) {\n person (id: $id) {\n id\n name\n }\n}",
+ "variables": {
+ "id": "cGVvcGxlOjI="
+ }
+ }
+ },
+ {
+ "descriptionOfTest": "Normal request with basic auth",
+ "graphql": {
+ "method": "get",
+ "url": "https://camunda.io/http-endpoint",
+ "connectionTimeoutInSeconds": "0",
+ "query": "query Root($id: ID) {\n person (id: $id) {\n id\n name\n }\n}",
+ "variables": {
+ "id": "cGVvcGxlOjI="
+ }
+ }
+ },
+ {
+ "descriptionOfTest": "Normal request with bearer auth",
+ "authentication": {
+ "type": "bearer",
+ "token": "secrets.MY_TOKEN"
+ },
+ "graphql": {
+ "method": "get",
+ "url": "https://camunda.io/http-endpoint",
+ "connectionTimeoutInSeconds": "30",
+ "query": "query Root($id: ID) {\n person (id: $id) {\n id\n name\n }\n}",
+ "variables": {
+ "id": "cGVvcGxlOjI="
+ }
+ }
+ },
+ {
+ "descriptionOfTest": "Normal request with no variables",
+ "authentication": {
+ "type": "bearer",
+ "token": "secrets.MY_TOKEN"
+ },
+ "graphql": {
+ "method": "get",
+ "url": "https://camunda.io/http-endpoint",
+ "connectionTimeoutInSeconds": "200",
+ "query": "query Root($id: ID) {\n person (id: $id) {\n id\n name\n }\n}"
+ }
+ },
+ {
+ "descriptionOfTest": "Normal request with empty variables",
+ "authentication": {
+ "type": "bearer",
+ "token": "secrets.MY_TOKEN"
+ },
+ "graphql": {
+ "method": "get",
+ "url": "https://camunda.io/http-endpoint",
+ "connectionTimeoutInSeconds": "0",
+ "query": "query Root($id: ID) {\n person (id: $id) {\n id\n name\n }\n}",
+ "variables": {}
+ }
+ },
+ {
+ "descriptionOfTest": "Normal request with connectionTimeoutInSeconds",
+ "authentication": {
+ "type": "bearer",
+ "token": "secrets.MY_TOKEN"
+ },
+ "graphql": {
+ "method": "get",
+ "url": "https://camunda.io/http-endpoint",
+ "connectionTimeoutInSeconds": "50",
+ "query": "query Root($id: ID) {\n person (id: $id) {\n id\n name\n }\n}",
+ "variables": {
+ "id": "cGVvcGxlOjI="
+ }
+ }
+ }
+]
diff --git a/connectors/http-json/pom.xml b/connectors/http-json/pom.xml
index 01e5d54844..38b6cab23a 100644
--- a/connectors/http-json/pom.xml
+++ b/connectors/http-json/pom.xml
@@ -62,6 +62,11 @@ limitations under the License.
org.slf4j
jcl-over-slf4j
+
+
+ io.camunda.connector
+ connectors-common-library
+
diff --git a/connectors/http-json/src/main/java/io/camunda/connector/http/HttpJsonFunction.java b/connectors/http-json/src/main/java/io/camunda/connector/http/HttpJsonFunction.java
index 6bd71835a5..4c609728eb 100644
--- a/connectors/http-json/src/main/java/io/camunda/connector/http/HttpJsonFunction.java
+++ b/connectors/http-json/src/main/java/io/camunda/connector/http/HttpJsonFunction.java
@@ -21,9 +21,9 @@
import io.camunda.connector.api.annotation.OutboundConnector;
import io.camunda.connector.api.outbound.OutboundConnectorContext;
import io.camunda.connector.api.outbound.OutboundConnectorFunction;
+import io.camunda.connector.common.constants.Constants;
import io.camunda.connector.http.components.GsonComponentSupplier;
import io.camunda.connector.http.components.HttpTransportComponentSupplier;
-import io.camunda.connector.http.constants.Constants;
import io.camunda.connector.http.model.HttpJsonRequest;
import io.camunda.connector.impl.config.ConnectorConfigurationUtil;
import java.io.IOException;
@@ -63,7 +63,8 @@ public HttpJsonFunction(
}
@Override
- public Object execute(final OutboundConnectorContext context) throws IOException {
+ public Object execute(final OutboundConnectorContext context)
+ throws IOException, InstantiationException, IllegalAccessException {
final var json = context.getVariables();
final var request = gson.fromJson(json, HttpJsonRequest.class);
diff --git a/connectors/http-json/src/main/java/io/camunda/connector/http/HttpRequestMapper.java b/connectors/http-json/src/main/java/io/camunda/connector/http/HttpRequestMapper.java
index 2de84be953..d55d7dea62 100644
--- a/connectors/http-json/src/main/java/io/camunda/connector/http/HttpRequestMapper.java
+++ b/connectors/http-json/src/main/java/io/camunda/connector/http/HttpRequestMapper.java
@@ -18,25 +18,23 @@
import static org.apache.http.entity.ContentType.APPLICATION_JSON;
-import com.google.api.client.http.AbstractHttpContent;
import com.google.api.client.http.GenericUrl;
-import com.google.api.client.http.HttpContent;
import com.google.api.client.http.HttpHeaders;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpRequestFactory;
import com.google.api.client.http.UrlEncodedContent;
import com.google.api.client.http.json.JsonHttpContent;
import com.google.gson.Gson;
-import io.camunda.connector.http.auth.OAuthAuthentication;
-import io.camunda.connector.http.auth.ProxyOAuthHelper;
+import io.camunda.connector.common.auth.OAuthAuthentication;
+import io.camunda.connector.common.constants.Constants;
+import io.camunda.connector.common.model.CommonRequest;
+import io.camunda.connector.common.model.HttpRequestBuilder;
import io.camunda.connector.http.components.GsonComponentSupplier;
-import io.camunda.connector.http.constants.Constants;
import io.camunda.connector.http.model.HttpJsonRequest;
-import io.camunda.connector.http.model.HttpRequestBuilder;
import io.camunda.connector.impl.ConnectorInputException;
import java.io.IOException;
-import java.io.OutputStream;
-import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
import javax.validation.ValidationException;
public class HttpRequestMapper {
@@ -45,35 +43,6 @@ public class HttpRequestMapper {
private HttpRequestMapper() {}
- public static HttpRequest toRequestViaProxy(
- final HttpRequestFactory requestFactory,
- final HttpJsonRequest request,
- final String proxyFunctionUrl)
- throws IOException {
- // Using the JsonHttpContent cannot work with an element on the root content,
- // hence write it ourselves:
- final String contentAsJson = gson.toJson(request);
- HttpContent content =
- new AbstractHttpContent(Constants.APPLICATION_JSON_CHARSET_UTF_8) {
- public void writeTo(OutputStream outputStream) throws IOException {
- outputStream.write(contentAsJson.getBytes(StandardCharsets.UTF_8));
- }
- };
-
- HttpRequest httpRequest =
- new HttpRequestBuilder()
- .method(Constants.POST)
- .genericUrl(new GenericUrl(proxyFunctionUrl))
- .content(content)
- .connectionTimeoutInSeconds(request.getConnectionTimeoutInSeconds())
- .followRedirects(false)
- .build(requestFactory);
-
- ProxyOAuthHelper.addOauthHeaders(
- httpRequest, ProxyOAuthHelper.initializeCredentials(proxyFunctionUrl));
- return httpRequest;
- }
-
public static HttpRequest toOAuthHttpRequest(
final HttpRequestFactory requestFactory, final HttpJsonRequest request) throws IOException {
@@ -89,21 +58,34 @@ public static HttpRequest toOAuthHttpRequest(
return new HttpRequestBuilder()
.method(Constants.POST)
.genericUrl(new GenericUrl(authentication.getOauthTokenEndpoint()))
- .content(new UrlEncodedContent(authentication.getDataForAuthRequestBody()))
+ .content(new UrlEncodedContent(getDataForAuthRequestBody(authentication)))
.headers(headers)
.connectionTimeoutInSeconds(request.getConnectionTimeoutInSeconds())
.followRedirects(false)
.build(requestFactory);
}
+ public static Map getDataForAuthRequestBody(OAuthAuthentication authentication) {
+ Map data = new HashMap<>();
+ data.put(Constants.GRANT_TYPE, authentication.getGrantType());
+ data.put(Constants.AUDIENCE, authentication.getAudience());
+ data.put(Constants.SCOPE, authentication.getScopes());
+
+ if (Constants.CREDENTIALS_BODY.equals(authentication.getClientAuthentication())) {
+ data.put(Constants.CLIENT_ID, authentication.getClientId());
+ data.put(Constants.CLIENT_SECRET, authentication.getClientSecret());
+ }
+ return data;
+ }
+
public static HttpRequest toHttpRequest(
- final HttpRequestFactory requestFactory, final HttpJsonRequest request) throws IOException {
+ final HttpRequestFactory requestFactory, final CommonRequest request) throws IOException {
return toHttpRequest(requestFactory, request, null);
}
public static HttpRequest toHttpRequest(
final HttpRequestFactory requestFactory,
- final HttpJsonRequest request,
+ final CommonRequest request,
final String bearerToken)
throws IOException {
// TODO: add more holistic solution
@@ -131,7 +113,7 @@ public static HttpRequest toHttpRequest(
.build(requestFactory);
}
- private static HttpHeaders createHeaders(final HttpJsonRequest request, String bearerToken) {
+ private static HttpHeaders createHeaders(final CommonRequest request, String bearerToken) {
final HttpHeaders httpHeaders = new HttpHeaders();
if (request.hasBody()) {
httpHeaders.setContentType(APPLICATION_JSON.getMimeType());
diff --git a/connectors/http-json/src/main/java/io/camunda/connector/http/HttpResponseMapper.java b/connectors/http-json/src/main/java/io/camunda/connector/http/HttpResponseMapper.java
deleted file mode 100644
index 8efbaf46c7..0000000000
--- a/connectors/http-json/src/main/java/io/camunda/connector/http/HttpResponseMapper.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * 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;
-
-import com.google.api.client.http.HttpResponse;
-import com.google.gson.Gson;
-import io.camunda.connector.http.components.GsonComponentSupplier;
-import io.camunda.connector.http.model.HttpJsonResult;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.Reader;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-public class HttpResponseMapper {
-
- private static final Logger LOGGER = LoggerFactory.getLogger(HttpResponseMapper.class);
- private static final Gson gson = GsonComponentSupplier.gsonInstance();
-
- private HttpResponseMapper() {}
-
- public static HttpJsonResult toHttpJsonResponse(final HttpResponse externalResponse) {
- final HttpJsonResult httpJsonResult = new HttpJsonResult();
- httpJsonResult.setStatus(externalResponse.getStatusCode());
- final Map headers = new HashMap<>();
- externalResponse
- .getHeaders()
- .forEach(
- (k, v) -> {
- if (v instanceof List && ((List>) v).size() == 1) {
- headers.put(k, ((List>) v).get(0));
- } else {
- headers.put(k, v);
- }
- });
- httpJsonResult.setHeaders(headers);
- try (InputStream content = externalResponse.getContent();
- Reader reader = new InputStreamReader(content)) {
- final Object body = gson.fromJson(reader, Object.class);
- if (body != null) {
- httpJsonResult.setBody(body);
- }
- } catch (final Exception e) {
- LOGGER.error("Failed to parse external response: {}", externalResponse, e);
- }
- return httpJsonResult;
- }
-}
diff --git a/connectors/http-json/src/main/java/io/camunda/connector/http/HttpService.java b/connectors/http-json/src/main/java/io/camunda/connector/http/HttpService.java
index 220ee5e3d9..3caafa5f9c 100644
--- a/connectors/http-json/src/main/java/io/camunda/connector/http/HttpService.java
+++ b/connectors/http-json/src/main/java/io/camunda/connector/http/HttpService.java
@@ -19,21 +19,19 @@
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpRequestFactory;
import com.google.api.client.http.HttpResponse;
-import com.google.api.client.http.HttpResponseException;
import com.google.gson.Gson;
-import com.google.gson.JsonObject;
import io.camunda.connector.api.error.ConnectorException;
-import io.camunda.connector.http.auth.CustomAuthentication;
-import io.camunda.connector.http.auth.OAuthAuthentication;
-import io.camunda.connector.http.model.ErrorResponse;
+import io.camunda.connector.common.auth.CustomAuthentication;
+import io.camunda.connector.common.auth.OAuthAuthentication;
+import io.camunda.connector.common.services.AuthenticationService;
+import io.camunda.connector.common.services.HTTPProxyService;
+import io.camunda.connector.common.services.HTTPService;
import io.camunda.connector.http.model.HttpJsonRequest;
import io.camunda.connector.http.model.HttpJsonResult;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
-import java.util.HashMap;
-import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -51,24 +49,28 @@ public HttpService(
this.proxyFunctionUrl = proxyFunctionUrl;
}
- public Object executeConnectorRequest(final HttpJsonRequest request) throws IOException {
+ public Object executeConnectorRequest(final HttpJsonRequest request)
+ throws IOException, InstantiationException, IllegalAccessException {
return proxyFunctionUrl == null
? executeRequestDirectly(request)
: executeRequestViaProxy(request);
}
- private HttpJsonResult executeRequestDirectly(HttpJsonRequest request) throws IOException {
+ private HttpJsonResult executeRequestDirectly(HttpJsonRequest request)
+ throws IOException, InstantiationException, IllegalAccessException {
String bearerToken = null;
+ HTTPService httpService = new HTTPService(gson);
+ AuthenticationService authService = new AuthenticationService(gson, requestFactory);
if (request.getAuthentication() != null) {
if (request.getAuthentication() instanceof OAuthAuthentication) {
- bearerToken = getTokenFromOAuthRequest(request);
+ bearerToken = getTokenFromOAuthRequest(request, httpService, authService);
} else if (request.getAuthentication() instanceof CustomAuthentication) {
final var authentication = (CustomAuthentication) request.getAuthentication();
final var httpRequest =
HttpRequestMapper.toHttpRequest(requestFactory, authentication.getRequest());
- HttpResponse httpResponse = executeHttpRequest(httpRequest);
+ HttpResponse httpResponse = httpService.executeHttpRequest(httpRequest);
if (httpResponse.isSuccessStatusCode()) {
- fillRequestFromCustomAuthResponseData(request, authentication, httpResponse);
+ authService.fillRequestFromCustomAuthResponseData(request, authentication, httpResponse);
} else {
throw new RuntimeException(
"Authenticate is fail; status code : ["
@@ -80,74 +82,27 @@ private HttpJsonResult executeRequestDirectly(HttpJsonRequest request) throws IO
}
}
HttpRequest httpRequest = HttpRequestMapper.toHttpRequest(requestFactory, request, bearerToken);
- HttpResponse httpResponse = executeHttpRequest(httpRequest, false);
- return HttpResponseMapper.toHttpJsonResponse(httpResponse);
+ HttpResponse httpResponse = httpService.executeHttpRequest(httpRequest, false);
+ return httpService.toHttpJsonResponse(httpResponse, HttpJsonResult.class);
}
- private void fillRequestFromCustomAuthResponseData(
- final HttpJsonRequest request,
- final CustomAuthentication authentication,
- final HttpResponse httpResponse)
+ private String getTokenFromOAuthRequest(
+ final HttpJsonRequest connectorRequest,
+ final HTTPService httpService,
+ final AuthenticationService authService)
throws IOException {
- String strResponse = httpResponse.parseAsString();
- Map headers =
- ResponseParser.extractPropertiesFromBody(authentication.getOutputHeaders(), strResponse);
- if (headers != null) {
- if (!request.hasHeaders()) {
- request.setHeaders(new HashMap<>());
- }
- request.getHeaders().putAll(headers);
- }
-
- Map body =
- ResponseParser.extractPropertiesFromBody(authentication.getOutputBody(), strResponse);
- if (body != null) {
- if (!request.hasBody()) {
- request.setBody(new Object());
- }
- JsonObject requestBody = gson.toJsonTree(request.getBody()).getAsJsonObject();
- // for now, we can add only string property to body, example of this object :
- // "{"key":"value"}" but we can expand this method
- body.forEach(requestBody::addProperty);
- request.setBody(gson.fromJson(gson.toJson(requestBody), Object.class));
- }
- }
-
- private String getTokenFromOAuthRequest(final HttpJsonRequest request) throws IOException {
- final HttpRequest httpRequest = HttpRequestMapper.toOAuthHttpRequest(requestFactory, request);
- final HttpResponse oauthResponse = executeHttpRequest(httpRequest);
- return ResponseParser.extractOAuthAccessToken(oauthResponse);
- }
-
- private HttpResponse executeHttpRequest(HttpRequest externalRequest) throws IOException {
- return executeHttpRequest(externalRequest, false);
- }
-
- private HttpResponse executeHttpRequest(HttpRequest externalRequest, boolean isProxyCall)
- throws IOException {
- try {
- return externalRequest.execute();
- } catch (HttpResponseException hrex) {
- var errorCode = String.valueOf(hrex.getStatusCode());
- var errorMessage = hrex.getMessage();
- if (isProxyCall && hrex.getContent() != null) {
- try {
- final var errorContent = gson.fromJson(hrex.getContent(), ErrorResponse.class);
- errorCode = errorContent.getErrorCode();
- errorMessage = errorContent.getError();
- } catch (Exception e) {
- // cannot be loaded as JSON, ignore and use plain message
- }
- }
- throw new ConnectorException(errorCode, errorMessage, hrex);
- }
+ final HttpRequest oauthRequest = authService.createOAuthRequest(connectorRequest);
+ final HttpResponse oauthResponse = httpService.executeHttpRequest(oauthRequest);
+ return authService.extractOAuthAccessToken(oauthResponse);
}
private HttpJsonResult executeRequestViaProxy(HttpJsonRequest request) throws IOException {
HttpRequest httpRequest =
- HttpRequestMapper.toRequestViaProxy(requestFactory, request, proxyFunctionUrl);
+ HTTPProxyService.toRequestViaProxy(gson, requestFactory, request, proxyFunctionUrl);
+
+ HTTPService httpService = new HTTPService(gson);
- HttpResponse httpResponse = executeHttpRequest(httpRequest, true);
+ HttpResponse httpResponse = httpService.executeHttpRequest(httpRequest, true);
try (InputStream responseContentStream = httpResponse.getContent();
Reader reader = new InputStreamReader(responseContentStream)) {
diff --git a/connectors/http-json/src/main/java/io/camunda/connector/http/components/GsonComponentSupplier.java b/connectors/http-json/src/main/java/io/camunda/connector/http/components/GsonComponentSupplier.java
index 28efdd427d..e590087277 100644
--- a/connectors/http-json/src/main/java/io/camunda/connector/http/components/GsonComponentSupplier.java
+++ b/connectors/http-json/src/main/java/io/camunda/connector/http/components/GsonComponentSupplier.java
@@ -21,12 +21,12 @@
import com.google.gson.GsonBuilder;
import com.google.gson.ToNumberPolicy;
import com.google.gson.typeadapters.RuntimeTypeAdapterFactory;
-import io.camunda.connector.http.auth.Authentication;
-import io.camunda.connector.http.auth.BasicAuthentication;
-import io.camunda.connector.http.auth.BearerAuthentication;
-import io.camunda.connector.http.auth.CustomAuthentication;
-import io.camunda.connector.http.auth.NoAuthentication;
-import io.camunda.connector.http.auth.OAuthAuthentication;
+import io.camunda.connector.common.auth.Authentication;
+import io.camunda.connector.common.auth.BasicAuthentication;
+import io.camunda.connector.common.auth.BearerAuthentication;
+import io.camunda.connector.common.auth.CustomAuthentication;
+import io.camunda.connector.common.auth.NoAuthentication;
+import io.camunda.connector.common.auth.OAuthAuthentication;
public class GsonComponentSupplier {
diff --git a/connectors/http-json/src/main/java/io/camunda/connector/http/model/HttpJsonRequest.java b/connectors/http-json/src/main/java/io/camunda/connector/http/model/HttpJsonRequest.java
index a407e5f540..5f8ac6b623 100644
--- a/connectors/http-json/src/main/java/io/camunda/connector/http/model/HttpJsonRequest.java
+++ b/connectors/http-json/src/main/java/io/camunda/connector/http/model/HttpJsonRequest.java
@@ -16,148 +16,6 @@
*/
package io.camunda.connector.http.model;
-import com.google.common.base.Objects;
-import io.camunda.connector.api.annotation.Secret;
-import io.camunda.connector.http.auth.Authentication;
-import java.util.Map;
-import javax.validation.Valid;
-import javax.validation.constraints.NotBlank;
-import javax.validation.constraints.Pattern;
+import io.camunda.connector.common.model.CommonRequest;
-public class HttpJsonRequest {
-
- @NotBlank @Secret private String method;
-
- @NotBlank
- @Pattern(regexp = "^(http://|https://|secrets).*$")
- @Secret
- private String url;
-
- @Valid @Secret private Authentication authentication;
- @Secret private Map queryParameters;
- @Secret private Map headers;
-
- @Pattern(regexp = "^([0-9]*$)|(secrets.*$)")
- @Secret
- private String connectionTimeoutInSeconds;
-
- @Secret private Object body;
-
- public boolean hasAuthentication() {
- return authentication != null;
- }
-
- public boolean hasQueryParameters() {
- return queryParameters != null;
- }
-
- public boolean hasHeaders() {
- return headers != null;
- }
-
- public boolean hasBody() {
- return body != null;
- }
-
- public String getMethod() {
- return method;
- }
-
- public void setMethod(final String method) {
- this.method = method;
- }
-
- public String getUrl() {
- return url;
- }
-
- public void setUrl(final String url) {
- this.url = url;
- }
-
- public Authentication getAuthentication() {
- return authentication;
- }
-
- public void setAuthentication(final Authentication authentication) {
- this.authentication = authentication;
- }
-
- public Map getQueryParameters() {
- return queryParameters;
- }
-
- public void setQueryParameters(final Map queryParameters) {
- this.queryParameters = queryParameters;
- }
-
- public Map getHeaders() {
- return headers;
- }
-
- public void setHeaders(final Map headers) {
- this.headers = headers;
- }
-
- public String getConnectionTimeoutInSeconds() {
- return connectionTimeoutInSeconds;
- }
-
- public void setConnectionTimeoutInSeconds(final String connectionTimeoutInSeconds) {
- this.connectionTimeoutInSeconds = connectionTimeoutInSeconds;
- }
-
- public Object getBody() {
- return body;
- }
-
- public void setBody(final Object body) {
- this.body = body;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) {
- return true;
- }
- if (o == null || getClass() != o.getClass()) {
- return false;
- }
- final HttpJsonRequest that = (HttpJsonRequest) o;
- return java.util.Objects.equals(method, that.method)
- && java.util.Objects.equals(url, that.url)
- && java.util.Objects.equals(authentication, that.authentication)
- && java.util.Objects.equals(queryParameters, that.queryParameters)
- && java.util.Objects.equals(headers, that.headers)
- && java.util.Objects.equals(connectionTimeoutInSeconds, that.connectionTimeoutInSeconds)
- && java.util.Objects.equals(body, that.body);
- }
-
- @Override
- public int hashCode() {
- return Objects.hashCode(method, url, authentication, queryParameters, headers, body);
- }
-
- @Override
- public String toString() {
- return "HttpJsonRequest{"
- + "method='"
- + method
- + '\''
- + ", url='"
- + url
- + '\''
- + ", authentication="
- + authentication
- + ", queryParameters="
- + queryParameters
- + ", headers="
- + headers
- + ", connectionTimeoutInSeconds='"
- + connectionTimeoutInSeconds
- + '\''
- + ", body="
- + body
- + '}';
- }
-}
+public class HttpJsonRequest extends CommonRequest {}
diff --git a/connectors/http-json/src/main/java/io/camunda/connector/http/model/HttpJsonResult.java b/connectors/http-json/src/main/java/io/camunda/connector/http/model/HttpJsonResult.java
index 22c22e08c2..5d208084d1 100644
--- a/connectors/http-json/src/main/java/io/camunda/connector/http/model/HttpJsonResult.java
+++ b/connectors/http-json/src/main/java/io/camunda/connector/http/model/HttpJsonResult.java
@@ -16,59 +16,6 @@
*/
package io.camunda.connector.http.model;
-import com.google.common.base.Objects;
-import java.util.Map;
+import io.camunda.connector.common.model.CommonResult;
-public class HttpJsonResult {
- private int status;
- private Map headers;
- private Object body;
-
- public int getStatus() {
- return status;
- }
-
- public void setStatus(int status) {
- this.status = status;
- }
-
- public Map getHeaders() {
- return headers;
- }
-
- public void setHeaders(Map headers) {
- this.headers = headers;
- }
-
- public Object getBody() {
- return body;
- }
-
- public void setBody(Object body) {
- this.body = body;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) {
- return true;
- }
- if (o == null || getClass() != o.getClass()) {
- return false;
- }
- HttpJsonResult that = (HttpJsonResult) o;
- return status == that.status
- && Objects.equal(headers, that.headers)
- && Objects.equal(body, that.body);
- }
-
- @Override
- public int hashCode() {
- return Objects.hashCode(status, headers, body);
- }
-
- @Override
- public String toString() {
- return "HttpJsonResult{" + "status=" + status + ", headers=" + headers + ", body=" + body + '}';
- }
-}
+public class HttpJsonResult extends CommonResult {}
diff --git a/connectors/http-json/src/test/java/io/camunda/connector/http/HttpJsonFunctionProxyTest.java b/connectors/http-json/src/test/java/io/camunda/connector/http/HttpJsonFunctionProxyTest.java
index 16fd187198..a99d49a607 100644
--- a/connectors/http-json/src/test/java/io/camunda/connector/http/HttpJsonFunctionProxyTest.java
+++ b/connectors/http-json/src/test/java/io/camunda/connector/http/HttpJsonFunctionProxyTest.java
@@ -32,7 +32,7 @@
import com.google.api.client.http.HttpResponse;
import com.google.api.client.http.HttpResponseException;
import io.camunda.connector.api.error.ConnectorException;
-import io.camunda.connector.http.constants.Constants;
+import io.camunda.connector.common.constants.Constants;
import io.camunda.connector.http.model.HttpJsonResult;
import io.camunda.connector.test.outbound.OutboundConnectorContextBuilder;
import java.io.ByteArrayInputStream;
@@ -67,7 +67,8 @@ public void setup() {
@ParameterizedTest(name = "Executing test case: {0}")
@MethodSource("successCases")
- void shouldReturnResult_WhenExecuted(final String input) throws IOException {
+ void shouldReturnResult_WhenExecuted(final String input)
+ throws IOException, InstantiationException, IllegalAccessException {
// given - minimal required entity
final var context =
OutboundConnectorContextBuilder.create().variables(input).secrets(name -> "foo").build();
diff --git a/connectors/http-json/src/test/java/io/camunda/connector/http/HttpJsonFunctionSecretsTest.java b/connectors/http-json/src/test/java/io/camunda/connector/http/HttpJsonFunctionSecretsTest.java
index 1250f7dc61..b9291c3d67 100644
--- a/connectors/http-json/src/test/java/io/camunda/connector/http/HttpJsonFunctionSecretsTest.java
+++ b/connectors/http-json/src/test/java/io/camunda/connector/http/HttpJsonFunctionSecretsTest.java
@@ -21,11 +21,11 @@
import com.google.gson.JsonObject;
import io.camunda.connector.api.outbound.OutboundConnectorContext;
-import io.camunda.connector.http.auth.Authentication;
-import io.camunda.connector.http.auth.BasicAuthentication;
-import io.camunda.connector.http.auth.BearerAuthentication;
-import io.camunda.connector.http.auth.NoAuthentication;
-import io.camunda.connector.http.auth.OAuthAuthentication;
+import io.camunda.connector.common.auth.Authentication;
+import io.camunda.connector.common.auth.BasicAuthentication;
+import io.camunda.connector.common.auth.BearerAuthentication;
+import io.camunda.connector.common.auth.NoAuthentication;
+import io.camunda.connector.common.auth.OAuthAuthentication;
import io.camunda.connector.http.model.HttpJsonRequest;
import java.io.IOException;
import java.util.stream.Stream;
diff --git a/connectors/http-json/src/test/java/io/camunda/connector/http/HttpJsonFunctionTest.java b/connectors/http-json/src/test/java/io/camunda/connector/http/HttpJsonFunctionTest.java
index 2272d4dffd..ce3b68b661 100644
--- a/connectors/http-json/src/test/java/io/camunda/connector/http/HttpJsonFunctionTest.java
+++ b/connectors/http-json/src/test/java/io/camunda/connector/http/HttpJsonFunctionTest.java
@@ -79,7 +79,8 @@ public void setup() {
@ParameterizedTest(name = "Executing test case: {0}")
@MethodSource("successCases")
- void shouldReturnResult_WhenExecuted(final String input) throws IOException {
+ void shouldReturnResult_WhenExecuted(final String input)
+ throws IOException, InstantiationException, IllegalAccessException {
// given - minimal required entity
Object functionCallResponseAsObject = arrange(input);
@@ -92,7 +93,8 @@ void shouldReturnResult_WhenExecuted(final String input) throws IOException {
@ParameterizedTest(name = "Executing test case: {0}")
@MethodSource("successCasesCustomAuth")
- void shouldReturnResultCustom_WhenExecuted(final String input) throws IOException {
+ void shouldReturnResultCustom_WhenExecuted(final String input)
+ throws IOException, InstantiationException, IllegalAccessException {
String response =
"{\"token\":\"eyJhbJNtIbehBWQLAGapcHIctws7gavjTCSCCC0Xd5sIn7DaB52Pwmabdj-9AkrVru_fZwLQseAq38n1-DkiyAaewxB0VbQgQ\",\"user\":{\"id\":331707,\"principalId\":331707,\"deleted\":false,\"permissions\":[{\"id\":13044559,\"resourceType\":\"processdiscovery\"},{\"id\":13044527,\"resourceType\":\"credentials\"},],\"emailVerified\":true,\"passwordSet\":true},\"tenantUuid\":\"08b93cfe-a6dd-4d6b-94aa-9369fdd2a026\"}";
@@ -109,7 +111,8 @@ void shouldReturnResultCustom_WhenExecuted(final String input) throws IOExceptio
@ParameterizedTest(name = "Executing test case: {0}")
@MethodSource("successCasesOauth")
- void shouldReturnResultOAuth_WhenExecuted(final String input) throws IOException {
+ void shouldReturnResultOAuth_WhenExecuted(final String input)
+ throws IOException, InstantiationException, IllegalAccessException {
Object functionCallResponseAsObject = arrange(input);
// then
@@ -119,7 +122,8 @@ void shouldReturnResultOAuth_WhenExecuted(final String input) throws IOException
.containsValue(APPLICATION_JSON.getMimeType());
}
- private Object arrange(String input) throws IOException {
+ private Object arrange(String input)
+ throws IOException, InstantiationException, IllegalAccessException {
final var context =
OutboundConnectorContextBuilder.create().variables(input).secrets(name -> "foo").build();
when(requestFactory.buildRequest(
@@ -149,7 +153,8 @@ void shouldReturnFallbackResult_WhenMalformedRequest(final String input) {
}
@Test
- void execute_shouldReturnNullFieldWhenResponseWithContainNullField() throws IOException {
+ void execute_shouldReturnNullFieldWhenResponseWithContainNullField()
+ throws IOException, InstantiationException, IllegalAccessException {
// given request, and response body with null field value
final var request =
"{ \"method\": \"get\", \"url\": \"https://camunda.io/http-endpoint\", \"authentication\": { \"type\": \"noAuth\" } }";
@@ -177,7 +182,8 @@ void execute_shouldReturnNullFieldWhenResponseWithContainNullField() throws IOEx
@ParameterizedTest(name = "Executing test case: {0}")
@MethodSource("successCases")
- void execute_shouldSetConnectTime(final String input) throws IOException {
+ void execute_shouldSetConnectTime(final String input)
+ throws IOException, InstantiationException, IllegalAccessException {
// given - minimal required entity
final var context =
OutboundConnectorContextBuilder.create().variables(input).secrets(name -> "foo").build();
diff --git a/connectors/http-json/src/test/java/io/camunda/connector/http/HttpServiceTest.java b/connectors/http-json/src/test/java/io/camunda/connector/http/HttpServiceTest.java
index 6ddce91274..e141ec4d37 100644
--- a/connectors/http-json/src/test/java/io/camunda/connector/http/HttpServiceTest.java
+++ b/connectors/http-json/src/test/java/io/camunda/connector/http/HttpServiceTest.java
@@ -33,7 +33,8 @@
import com.google.api.client.testing.http.MockHttpTransport;
import com.google.api.client.testing.http.MockLowLevelHttpRequest;
import com.google.api.client.testing.http.MockLowLevelHttpResponse;
-import io.camunda.connector.http.constants.Constants;
+import io.camunda.connector.common.constants.Constants;
+import io.camunda.connector.common.services.AuthenticationService;
import io.camunda.connector.http.model.HttpJsonRequest;
import io.camunda.connector.http.model.HttpJsonResult;
import io.camunda.connector.impl.config.ConnectorConfigurationUtil;
@@ -78,7 +79,8 @@ void checkIfOAuthBearerTokenIsAddedOnTheRequestHeader(final String input) throws
when(httpResponse.parseAsString()).thenReturn(ACCESS_TOKEN);
// when
- String bearerToken = ResponseParser.extractOAuthAccessToken(httpResponse);
+ AuthenticationService authenticationService = new AuthenticationService(gson, requestFactory);
+ String bearerToken = authenticationService.extractOAuthAccessToken(httpResponse);
HttpRequest request =
HttpRequestMapper.toHttpRequest(requestFactory, httpJsonRequest, bearerToken);
// check if the bearer token is correctly added on the header of the main request
@@ -88,7 +90,8 @@ void checkIfOAuthBearerTokenIsAddedOnTheRequestHeader(final String input) throws
@ParameterizedTest(name = "Executing test case: {0}")
@MethodSource("successCasesCustomAuth")
- void execute_shouldPassAllStepsAndParsing(final String input) throws IOException {
+ void execute_shouldPassAllStepsAndParsing(final String input)
+ throws IOException, InstantiationException, IllegalAccessException {
// given
final var context = OutboundConnectorContextBuilder.create().variables(input).build();
final var httpJsonRequest = gson.fromJson(context.getVariables(), HttpJsonRequest.class);
diff --git a/connectors/http-json/src/test/java/io/camunda/connector/http/auth/OAuthAuthenticationTest.java b/connectors/http-json/src/test/java/io/camunda/connector/http/auth/OAuthAuthenticationTest.java
index 54660f9b89..6c299c582a 100644
--- a/connectors/http-json/src/test/java/io/camunda/connector/http/auth/OAuthAuthenticationTest.java
+++ b/connectors/http-json/src/test/java/io/camunda/connector/http/auth/OAuthAuthenticationTest.java
@@ -27,10 +27,10 @@
import com.google.api.client.http.HttpRequestFactory;
import com.google.api.client.http.HttpResponse;
import com.google.api.client.testing.http.MockHttpTransport;
+import io.camunda.connector.common.constants.Constants;
+import io.camunda.connector.common.services.AuthenticationService;
import io.camunda.connector.http.BaseTest;
import io.camunda.connector.http.HttpRequestMapper;
-import io.camunda.connector.http.ResponseParser;
-import io.camunda.connector.http.constants.Constants;
import io.camunda.connector.http.model.HttpJsonRequest;
import io.camunda.connector.test.outbound.OutboundConnectorContextBuilder;
import java.io.IOException;
@@ -81,7 +81,8 @@ void checkOAuthBearerTokenFormat(final String input) throws IOException {
// check if the bearer token has the correct format and doesn't contain quotes
- assertFalse(ResponseParser.extractOAuthAccessToken(httpResponse).contains("\""));
+ AuthenticationService authenticationService = new AuthenticationService(gson, requestFactory);
+ assertFalse(authenticationService.extractOAuthAccessToken(httpResponse).contains("\""));
}
private static Stream successCasesOauth() throws IOException {
diff --git a/connectors/pom.xml b/connectors/pom.xml
index 3eb15d451d..e9fe6dcead 100644
--- a/connectors/pom.xml
+++ b/connectors/pom.xml
@@ -28,6 +28,8 @@
http-json
microsoft-teams
slack
+ graphql
+ connectors-common-library
diff --git a/pom.xml b/pom.xml
index e68737cdf6..81e9e7db36 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
io.camunda.connector
connector-parent
- 0.6.0-SNAPSHOT
+ 0.6.0-alpha3
@@ -183,6 +183,12 @@
${version.slack}
+
+ io.camunda.connector
+ connectors-common-library
+ ${project.version}
+
+
org.apache.commons