diff --git a/runtime/config-deployer-service/ballerina/APIClient.bal b/runtime/config-deployer-service/ballerina/APIClient.bal index 4b84e9733..6631b0ca4 100644 --- a/runtime/config-deployer-service/ballerina/APIClient.bal +++ b/runtime/config-deployer-service/ballerina/APIClient.bal @@ -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 { @@ -1526,7 +1526,6 @@ public class APIClient { map errors = {}; self.validateEndpointConfigurations(apkConf, errors); if (errors.length() > 0) { - log:printInfo(apkconfJson.toJsonString()); return e909029(errors); } return apkConf; diff --git a/runtime/config-deployer-service/ballerina/ConfigGenreatorClient.bal b/runtime/config-deployer-service/ballerina/ConfigGenreatorClient.bal index 35a4c92f4..e410cdb51 100644 --- a/runtime/config-deployer-service/ballerina/ConfigGenreatorClient.bal +++ b/runtime/config-deployer-service/ballerina/ConfigGenreatorClient.bal @@ -29,7 +29,7 @@ public class ConfigGeneratorClient { } else { apiType = 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; } @@ -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); @@ -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, content, "", fileName, true); } else { return e909008(); diff --git a/runtime/config-deployer-service/ballerina/modules/org.wso2.apk.config.model/API.bal b/runtime/config-deployer-service/ballerina/modules/org.wso2.apk.config.model/API.bal index 5722b4607..8638e05be 100644 --- a/runtime/config-deployer-service/ballerina/modules/org.wso2.apk.config.model/API.bal +++ b/runtime/config-deployer-service/ballerina/modules/org.wso2.apk.config.model/API.bal @@ -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)) ?: ""; } @@ -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: [] diff --git a/runtime/config-deployer-service/ballerina/modules/org.wso2.apk.config/RuntimeAPICommonUtil.bal b/runtime/config-deployer-service/ballerina/modules/org.wso2.apk.config/RuntimeAPICommonUtil.bal index 8c89e6cd0..6bf917f90 100644 --- a/runtime/config-deployer-service/ballerina/modules/org.wso2.apk.config/RuntimeAPICommonUtil.bal +++ b/runtime/config-deployer-service/ballerina/modules/org.wso2.apk.config/RuntimeAPICommonUtil.bal @@ -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()); @@ -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"] diff --git a/runtime/config-deployer-service/java/src/main/java/org/wso2/apk/config/APIConstants.java b/runtime/config-deployer-service/java/src/main/java/org/wso2/apk/config/APIConstants.java index 178436ddf..ed4a91e2a 100644 --- a/runtime/config-deployer-service/java/src/main/java/org/wso2/apk/config/APIConstants.java +++ b/runtime/config-deployer-service/java/src/main/java/org/wso2/apk/config/APIConstants.java @@ -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 GRAPHQL_SUPPORTED_METHOD_LIST = Collections.unmodifiableSet(new HashSet( 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 diff --git a/runtime/config-deployer-service/java/src/main/java/org/wso2/apk/config/RuntimeAPICommonUtil.java b/runtime/config-deployer-service/java/src/main/java/org/wso2/apk/config/RuntimeAPICommonUtil.java index 251c6b6a0..c383a740b 100644 --- a/runtime/config-deployer-service/java/src/main/java/org/wso2/apk/config/RuntimeAPICommonUtil.java +++ b/runtime/config-deployer-service/java/src/main/java/org/wso2/apk/config/RuntimeAPICommonUtil.java @@ -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 { @@ -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; } @@ -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 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() { } diff --git a/runtime/config-deployer-service/java/src/main/java/org/wso2/apk/config/definitions/GraphQLSchemaDefinition.java b/runtime/config-deployer-service/java/src/main/java/org/wso2/apk/config/definitions/GraphQLSchemaDefinition.java new file mode 100644 index 000000000..792189eda --- /dev/null +++ b/runtime/config-deployer-service/java/src/main/java/org/wso2/apk/config/definitions/GraphQLSchemaDefinition.java @@ -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 extractGraphQLOperationList(TypeDefinitionRegistry typeRegistry, String type) { + List operationArray = new ArrayList<>(); + Map operationList = typeRegistry.types(); + for (Map.Entry entry : operationList.entrySet()) { + Optional schemaDefinition = typeRegistry.schemaDefinition(); + if (schemaDefinition.isPresent()) { + List 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 entry, String graphQLType, + List operationArray) { + for (FieldDefinition fieldDef : ((ObjectTypeDefinition) entry.getValue()).getFieldDefinitions()) { + URITemplate operation = new URITemplate(); + operation.setVerb(graphQLType); + operation.setUriTemplate(fieldDef.getName()); + operationArray.add(operation); + } + } +} diff --git a/runtime/config-deployer-service/java/src/main/java/org/wso2/apk/config/definitions/OASParserUtil.java b/runtime/config-deployer-service/java/src/main/java/org/wso2/apk/config/definitions/OASParserUtil.java index 953fe56c9..3a582a6dc 100644 --- a/runtime/config-deployer-service/java/src/main/java/org/wso2/apk/config/definitions/OASParserUtil.java +++ b/runtime/config-deployer-service/java/src/main/java/org/wso2/apk/config/definitions/OASParserUtil.java @@ -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; @@ -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; @@ -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 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 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. *