Skip to content

Commit

Permalink
Add apk-conf generation for GraphQL APIs
Browse files Browse the repository at this point in the history
  • Loading branch information
sgayangi committed Feb 9, 2024
1 parent a21f998 commit 623f749
Show file tree
Hide file tree
Showing 8 changed files with 190 additions and 14 deletions.
3 changes: 1 addition & 2 deletions runtime/config-deployer-service/ballerina/APIClient.bal
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public class APIClient {
name: api.getName(),
basePath: api.getBasePath().length() > 0 ? api.getBasePath() : encodedString,
version: api.getVersion(),
'type: api.getType() == "" ? API_TYPE_REST : api.getType()
'type: api.getType() == "" ? API_TYPE_REST : api.getType().toUpperAscii()
};
string endpoint = api.getEndpoint();
if endpoint.length() > 0 {
Expand Down Expand Up @@ -1526,7 +1526,6 @@ public class APIClient {
map<string> errors = {};
self.validateEndpointConfigurations(apkConf, errors);
if (errors.length() > 0) {
log:printInfo(apkconfJson.toJsonString());
return e909029(errors);
}
return apkConf;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public class ConfigGeneratorClient {
} else {
apiType = <string>definitionBody.apiType;
}
if ALLOWED_API_TYPES.indexOf(apiType) is () {
if ALLOWED_API_TYPES.indexOf(apiType.toUpperAscii()) is () {
BadRequestError badRequest = {body: {code: 90091, message: "Invalid API Type"}};
return badRequest;
}
Expand All @@ -41,8 +41,7 @@ public class ConfigGeneratorClient {
}
if validateAndRetrieveDefinitionResult is runtimeapi:APIDefinitionValidationResponse {
if validateAndRetrieveDefinitionResult.isValid() {
runtimeapi:APIDefinition parser = validateAndRetrieveDefinitionResult.getParser();
runtimeModels:API apiFromDefinition = check parser.getAPIFromDefinition(validateAndRetrieveDefinitionResult.getContent());
runtimeModels:API apiFromDefinition = check runtimeUtil:RuntimeAPICommonUtil_getAPIFromDefinition(validateAndRetrieveDefinitionResult.getContent(), apiType);
apiFromDefinition.setType(apiType);
APIClient apiclient = new ();
APKConf generatedAPKConf = check apiclient.fromAPIModelToAPKConf(apiFromDefinition);
Expand Down Expand Up @@ -98,13 +97,13 @@ public class ConfigGeneratorClient {
if !typeAvailable {
return e909005("type");
}
if (ALLOWED_API_DEFINITION_TYPES.indexOf('type) is ()) {
if (ALLOWED_API_DEFINITION_TYPES.indexOf('type.toUpperAscii()) is ()) {
return e909006();
}
if url is string {
string retrieveDefinitionFromUrlResult = check self.retrieveDefinitionFromUrl(url);
validationResponse = runtimeUtil:RuntimeAPICommonUtil_validateOpenAPIDefinition('type, [], retrieveDefinitionFromUrlResult, fileName ?: "", true);
} else if fileName is string && content is byte[] {
} else if fileName is string && content is byte[] && content.length() > 0 {
validationResponse = runtimeUtil:RuntimeAPICommonUtil_validateOpenAPIDefinition('type, <byte[]>content, "", <string>fileName, true);
} else {
return e909008();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public distinct class API {
# The function that maps to the `getGraphQLSchema` method of `org.wso2.apk.config.model.API`.
#
# + return - The `string` value returning from the Java mapping.
public function getGraphQLSchema() returns string {
public isolated function getGraphQLSchema() returns string {
return java:toString(org_wso2_apk_config_model_API_getGraphQLSchema(self.jObj)) ?: "";
}

Expand Down Expand Up @@ -322,7 +322,7 @@ isolated function org_wso2_apk_config_model_API_getEnvironment(handle receiver)
paramTypes: []
} external;

function org_wso2_apk_config_model_API_getGraphQLSchema(handle receiver) returns handle = @java:Method {
isolated function org_wso2_apk_config_model_API_getGraphQLSchema(handle receiver) returns handle = @java:Method {
name: "getGraphQLSchema",
'class: "org.wso2.apk.config.model.API",
paramTypes: []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ public function RuntimeAPICommonUtil_generateUriTemplatesFromAPIDefinition(strin
# + arg0 - The `string` value required to map with the Java method parameter.
# + arg1 - The `string` value required to map with the Java method parameter.
# + return - The `orgwso2apkconfigmodel:API` or the `orgwso2apkconfigapi:APIManagementException` value returning from the Java mapping.
public function RuntimeAPICommonUtil_getAPIFromDefinition(string arg0, string arg1) returns orgwso2apkconfigmodel:API|orgwso2apkconfigapi:APIManagementException {
public isolated function RuntimeAPICommonUtil_getAPIFromDefinition(string arg0, string arg1) returns orgwso2apkconfigmodel:API|orgwso2apkconfigapi:APIManagementException {
handle|error externalObj = org_wso2_apk_config_RuntimeAPICommonUtil_getAPIFromDefinition(java:fromString(arg0), java:fromString(arg1));
if (externalObj is error) {
orgwso2apkconfigapi:APIManagementException e = error orgwso2apkconfigapi:APIManagementException(orgwso2apkconfigapi:APIMANAGEMENTEXCEPTION, externalObj, message = externalObj.message());
Expand Down Expand Up @@ -204,7 +204,7 @@ function org_wso2_apk_config_RuntimeAPICommonUtil_generateUriTemplatesFromAPIDef
paramTypes: ["java.lang.String", "java.lang.String"]
} external;

function org_wso2_apk_config_RuntimeAPICommonUtil_getAPIFromDefinition(handle arg0, handle arg1) returns handle|error = @java:Method {
isolated function org_wso2_apk_config_RuntimeAPICommonUtil_getAPIFromDefinition(handle arg0, handle arg1) returns handle|error = @java:Method {
name: "getAPIFromDefinition",
'class: "org.wso2.apk.config.RuntimeAPICommonUtil",
paramTypes: ["java.lang.String", "java.lang.String"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,13 @@ public final class APIConstants {
public static final String SQS_TRANSPORT_PROTOCOL_NAME = "sqs";
public static final String STOMP_TRANSPORT_PROTOCOL_NAME = "stomp";
public static final String REDIS_TRANSPORT_PROTOCOL_NAME = "redis";

// GraphQL related constants
public static final Set<String> GRAPHQL_SUPPORTED_METHOD_LIST = Collections.unmodifiableSet(new HashSet<String>(
Arrays.asList(new String[] { "QUERY", "MUTATION", "SUBSCRIPTION", "head", "options" })));
public static final String GRAPHQL_MUTATION = "MUTATION";
public static final String GRAPHQL_SUBSCRIPTION = "SUBSCRIPTION";
public static final String GRAPHQL_QUERY = "QUERY";

public enum ParserType {
REST, ASYNC, GRAPHQL
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,17 @@
import org.wso2.apk.config.api.APIDefinitionValidationResponse;
import org.wso2.apk.config.api.APIManagementException;
import org.wso2.apk.config.api.ExceptionCodes;
import org.wso2.apk.config.definitions.GraphQLSchemaDefinition;
import org.wso2.apk.config.definitions.OASParserUtil;
import org.wso2.apk.config.model.API;
import org.wso2.apk.config.model.URITemplate;

import graphql.schema.idl.SchemaParser;
import graphql.schema.idl.TypeDefinitionRegistry;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

public class RuntimeAPICommonUtil {
Expand Down Expand Up @@ -50,6 +56,17 @@ public static APIDefinitionValidationResponse validateOpenAPIDefinition(String t
} else if (apiDefinition != null) {
validationResponse = OASParserUtil.validateAPIDefinition(apiDefinition, returnContent);
}
} else if (APIConstants.ParserType.GRAPHQL.name().equals(type.toUpperCase())) {
if (fileName.endsWith(".graphql") || fileName.endsWith(".txt") ||
fileName.endsWith(".sdl")) {
validationResponse = OASParserUtil.validateGraphQLSchema(
new String(inputByteArray, StandardCharsets.UTF_8),
returnContent);
} else {
throw new APIManagementException("Unsupported extension type of file: " +
fileName,
ExceptionCodes.UNSUPPORTED_GRAPHQL_FILE_EXTENSION);
}
}
return validationResponse;
}
Expand Down Expand Up @@ -81,13 +98,37 @@ public static String generateDefinition(API api, String definition) throws APIMa

public static API getAPIFromDefinition(String definition, String apiType) throws APIManagementException {

APIDefinition parser = DefinitionParserFactory.getParser(apiType);
if (parser != null) {
return parser.getAPIFromDefinition(definition);
if (apiType.toUpperCase().equals(APIConstants.GRAPHQL_API)) {
return getGQLAPIFromDefinition(definition);
} else {
APIDefinition parser = DefinitionParserFactory.getParser(apiType);
if (parser != null) {
return parser.getAPIFromDefinition(definition);
}
}
throw new APIManagementException("Definition parser not found");
}

private static API getGQLAPIFromDefinition(String definition) {
SchemaParser schemaParser = new SchemaParser();
TypeDefinitionRegistry registry = schemaParser.parse(definition);
List<URITemplate> combinedUriTemplates = new ArrayList<>();

// Directly add all URI templates for query, mutation, and subscription into a
// combined list
combinedUriTemplates
.addAll(GraphQLSchemaDefinition.extractGraphQLOperationList(registry, APIConstants.GRAPHQL_QUERY));
combinedUriTemplates
.addAll(GraphQLSchemaDefinition.extractGraphQLOperationList(registry, APIConstants.GRAPHQL_MUTATION));
combinedUriTemplates.addAll(
GraphQLSchemaDefinition.extractGraphQLOperationList(registry, APIConstants.GRAPHQL_SUBSCRIPTION));

API api = new API();
api.setUriTemplates(combinedUriTemplates.toArray(new URITemplate[0]));
api.setGraphQLSchema(definition);
return api;
}

private RuntimeAPICommonUtil() {

}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
//
// Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com).
//
// WSO2 LLC. licenses this file to you under the Apache License,
// Version 2.0 (the "License"); you may not use this file except
// in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//
package org.wso2.apk.config.definitions;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wso2.apk.config.APIConstants;
import org.wso2.apk.config.model.URITemplate;
import org.wso2.apk.config.queryanalysis.GraphqlSchemaType;

import graphql.language.FieldDefinition;
import graphql.language.ObjectTypeDefinition;
import graphql.language.OperationTypeDefinition;
import graphql.language.SchemaDefinition;
import graphql.language.TypeDefinition;
import graphql.schema.idl.SchemaParser;
import graphql.schema.idl.TypeDefinitionRegistry;

public class GraphQLSchemaDefinition {
protected Log log = LogFactory.getLog(getClass());

/**
* Extract GraphQL Operations from given schema.
*
* @param typeRegistry graphQL Schema Type Registry
* @param type operation type string
* @return the arrayList of APIOperationsDTO
*/
public static List<URITemplate> extractGraphQLOperationList(TypeDefinitionRegistry typeRegistry, String type) {
List<URITemplate> operationArray = new ArrayList<>();
Map<java.lang.String, TypeDefinition> operationList = typeRegistry.types();
for (Map.Entry<String, TypeDefinition> entry : operationList.entrySet()) {
Optional<SchemaDefinition> schemaDefinition = typeRegistry.schemaDefinition();
if (schemaDefinition.isPresent()) {
List<OperationTypeDefinition> operationTypeList = schemaDefinition.get().getOperationTypeDefinitions();
for (OperationTypeDefinition operationTypeDefinition : operationTypeList) {
boolean canAddOperation = entry.getValue().getName()
.equalsIgnoreCase(operationTypeDefinition.getTypeName().getName()) &&
(type == null || type.equals(operationTypeDefinition.getName().toUpperCase()));
if (canAddOperation) {
addOperations(entry, operationTypeDefinition.getName().toUpperCase(), operationArray);
}
}
} else {
boolean canAddOperation = (entry.getValue().getName().equalsIgnoreCase(APIConstants.GRAPHQL_QUERY) ||
entry.getValue().getName().equalsIgnoreCase(APIConstants.GRAPHQL_MUTATION)
|| entry.getValue().getName().equalsIgnoreCase(APIConstants.GRAPHQL_SUBSCRIPTION)) &&
(type == null || type.equals(entry.getValue().getName().toUpperCase()));
if (canAddOperation) {
addOperations(entry, entry.getKey(), operationArray);
}
}
}
return operationArray;
}

/**
* @param entry Entry
* @param operationArray operationArray
*/
private static void addOperations(Map.Entry<String, TypeDefinition> entry, String graphQLType,
List<URITemplate> operationArray) {
for (FieldDefinition fieldDef : ((ObjectTypeDefinition) entry.getValue()).getFieldDefinitions()) {
URITemplate operation = new URITemplate();
operation.setVerb(graphQLType);
operation.setUriTemplate(fieldDef.getName());
operationArray.add(operation);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.node.ObjectNode;

import graphql.schema.GraphQLSchema;
import graphql.schema.idl.SchemaParser;
import graphql.schema.idl.TypeDefinitionRegistry;
import graphql.schema.idl.UnExecutableSchemaGenerator;
import graphql.schema.validation.SchemaValidationError;
import graphql.schema.validation.SchemaValidator;
import io.swagger.models.RefModel;
import io.swagger.models.RefPath;
import io.swagger.models.RefResponse;
Expand Down Expand Up @@ -54,6 +61,7 @@
import org.wso2.apk.config.api.APIDefinition;
import org.wso2.apk.config.api.APIDefinitionValidationResponse;
import org.wso2.apk.config.api.APIManagementException;
import org.wso2.apk.config.api.ErrorHandler;
import org.wso2.apk.config.api.ErrorItem;
import org.wso2.apk.config.api.ExceptionCodes;
import org.wso2.apk.config.api.Info;
Expand Down Expand Up @@ -618,6 +626,42 @@ public static APIDefinitionValidationResponse validateAPIDefinition(String apiDe
}
return validationResponse;
}

/**
* Validate graphQL Schema
*
* @return Validation response
*/
public static APIDefinitionValidationResponse validateGraphQLSchema(String apiDefinition,
boolean returnGraphQLSchemaContent) {
APIDefinitionValidationResponse validationResponse = new APIDefinitionValidationResponse();
ArrayList<ErrorHandler> errors = new ArrayList<>();
if (apiDefinition == "") {
validationResponse.setValid(false);
errors.add(new ErrorItem("Invalid API Definition", "API Definition is empty",
400, 400));
validationResponse.setErrorItems(errors);
return validationResponse;
}

SchemaParser schemaParser = new SchemaParser();
TypeDefinitionRegistry typeRegistry = schemaParser.parse(apiDefinition);
GraphQLSchema graphQLSchema = UnExecutableSchemaGenerator.makeUnExecutableSchema(typeRegistry);
SchemaValidator schemaValidation = new SchemaValidator();
Set<SchemaValidationError> validationErrors = schemaValidation.validateSchema(graphQLSchema);

if (validationErrors.toArray().length > 0) {
validationResponse.setValid(false);
errors.add(new ErrorItem("API Definition Validation Error", "API Definition is invalid", 400, 400));
validationResponse.setErrorItems(errors);
} else {
validationResponse.setValid(true);
validationResponse.setContent(apiDefinition);
}

return validationResponse;
}

/**
* This method removes the unsupported json blocks from the given json string.
*
Expand Down

0 comments on commit 623f749

Please sign in to comment.