Skip to content

Commit

Permalink
Add graphql api creation by SDL url and introspection
Browse files Browse the repository at this point in the history
  • Loading branch information
nisan-abeywickrama committed Jan 8, 2025
1 parent 80ca7fa commit 33f68da
Show file tree
Hide file tree
Showing 9 changed files with 314 additions and 59 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,12 @@ public enum ExceptionCodes implements ErrorHandler {
"GraphQL Schema cannot be empty or null"),
UNSUPPORTED_GRAPHQL_FILE_EXTENSION(900802, "Unsupported GraphQL Schema File Extension", 400,
"Unsupported extension. Only supported extensions are .graphql, .txt and .sdl"),

INVALID_GRAPHQL_FILE(900803, "GraphQL filename cannot be null or invalid", 400,
"GraphQL filename cannot be null or invalid"),
GENERATE_GRAPHQL_SCHEMA_FROM_INTROSPECTION_ERROR(900804, "Error while generating GraphQL schema from introspection",
500, "Error while generating GraphQL schema from introspection"),
RETRIEVE_GRAPHQL_SCHEMA_FROM_URL_ERROR(900805, "Error while retrieving GraphQL schema from URL", 500,
"Error while retrieving GraphQL schema from URL"),

// Oauth related codes
AUTH_GENERAL_ERROR(900900, "Authorization Error", 403, " Error in authorization"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@
import java.net.URLDecoder;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.rmi.RemoteException;
import java.security.*;
import java.security.cert.Certificate;
Expand Down Expand Up @@ -1404,6 +1405,18 @@ public static String getOpenAPIDefinitionFilePath(String apiName, String apiVers
apiName + RegistryConstants.PATH_SEPARATOR + apiVersion + RegistryConstants.PATH_SEPARATOR;
}

public static String getIntrospectionQuery() throws APIManagementException {
String introspectionQueryFilePath = "graphql/introspection_query.txt";
try (InputStream fileStream = APIUtil.class.getClassLoader().getResourceAsStream(introspectionQueryFilePath)) {
if (fileStream == null) {
throw new APIManagementException("File not found: " + introspectionQueryFilePath);
}
return IOUtils.toString(fileStream, StandardCharsets.UTF_8);
} catch (IOException e) {
throw new APIManagementException("Error reading introspection query file", e);
}
}

public static String getRevisionPath(String apiUUID, int revisionId) {
return APIConstants.API_REVISION_LOCATION + RegistryConstants.PATH_SEPARATOR +
apiUUID +
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
query IntrospectionQuery {
__schema {

queryType { name }
mutationType { name }
subscriptionType { name }
types {
...FullType
}
directives {
name
description

locations
args {
...InputValue
}
}
}
}

fragment FullType on __Type {
kind
name
description

fields(includeDeprecated: true) {
name
description
args {
...InputValue
}
type {
...TypeRef
}
isDeprecated
deprecationReason
}
inputFields {
...InputValue
}
interfaces {
...TypeRef
}
enumValues(includeDeprecated: true) {
name
description
isDeprecated
deprecationReason
}
possibleTypes {
...TypeRef
}
}

fragment InputValue on __InputValue {
name
description
type { ...TypeRef }
defaultValue


}

fragment TypeRef on __Type {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1618,7 +1618,7 @@ public static GraphQLValidationResponseDTO retrieveValidatedGraphqlSchemaFromArc
try {
String schemaDefinition = loadGraphqlSDLFile(pathToArchive);
GraphQLValidationResponseDTO graphQLValidationResponseDTO = PublisherCommonUtils
.validateGraphQLSchema(file.getName(), schemaDefinition);
.validateGraphQLSchema(file.getName(), schemaDefinition, null, false);
if (!graphQLValidationResponseDTO.isIsValid()) {
String errorMessage = "Error occurred while importing the API. Invalid GraphQL schema definition "
+ "found. " + graphQLValidationResponseDTO.getErrorMessage();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.reflect.TypeToken;
import graphql.introspection.IntrospectionResultToSchema;
import graphql.language.AstPrinter;
import graphql.language.Document;
import graphql.schema.GraphQLSchema;
import graphql.schema.idl.SchemaParser;
import graphql.schema.idl.TypeDefinitionRegistry;
Expand All @@ -38,6 +42,13 @@
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.util.EntityUtils;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
Expand Down Expand Up @@ -105,7 +116,10 @@
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Type;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
Expand All @@ -126,6 +140,7 @@ public class PublisherCommonUtils {

private static final Log log = LogFactory.getLog(PublisherCommonUtils.class);
public static final String SESSION_TIMEOUT_CONFIG_KEY = "sessionTimeOut";
private static String graphQLIntrospectionQuery = null;

/**
* Update API and API definition.
Expand Down Expand Up @@ -2000,47 +2015,58 @@ public static API addGraphQLSchema(API originalAPI, String schemaDefinition, API
* Validate GraphQL Schema.
*
* @param filename file name of the schema
* @param schema GraphQL schema
* @param schema GraphQL schema
* @param url URL of the schema
* @param useIntrospection use introspection to obtain schema
*/
public static GraphQLValidationResponseDTO validateGraphQLSchema(String filename, String schema)
throws APIManagementException {
public static GraphQLValidationResponseDTO validateGraphQLSchema(String filename, String schema, String url,
Boolean useIntrospection) throws APIManagementException {

String errorMessage;
GraphQLValidationResponseDTO validationResponse = new GraphQLValidationResponseDTO();
boolean isValid = false;
try {
if (filename.endsWith(".graphql") || filename.endsWith(".txt") || filename.endsWith(".sdl")) {
if (schema.isEmpty()) {
throw new APIManagementException("GraphQL Schema cannot be empty or null to validate it",
ExceptionCodes.GRAPHQL_SCHEMA_CANNOT_BE_NULL);
}
SchemaParser schemaParser = new SchemaParser();
TypeDefinitionRegistry typeRegistry = schemaParser.parse(schema);
GraphQLSchema graphQLSchema = UnExecutableSchemaGenerator.makeUnExecutableSchema(typeRegistry);
SchemaValidator schemaValidation = new SchemaValidator();
Set<SchemaValidationError> validationErrors = schemaValidation.validateSchema(graphQLSchema);

if (validationErrors.toArray().length > 0) {
errorMessage = "InValid Schema";
validationResponse.isValid(Boolean.FALSE);
validationResponse.errorMessage(errorMessage);
if (url != null && !url.isEmpty()) {
if (useIntrospection) {
schema = generateGraphQLSchemaFromIntrospection(url);
} else {
validationResponse.setIsValid(Boolean.TRUE);
GraphQLValidationResponseGraphQLInfoDTO graphQLInfo = new GraphQLValidationResponseGraphQLInfoDTO();
GraphQLSchemaDefinition graphql = new GraphQLSchemaDefinition();
List<URITemplate> operationList = graphql.extractGraphQLOperationList(typeRegistry, null);
List<APIOperationsDTO> operationArray = APIMappingUtil
.fromURITemplateListToOprationList(operationList);
graphQLInfo.setOperations(operationArray);
GraphQLSchemaDTO schemaObj = new GraphQLSchemaDTO();
schemaObj.setSchemaDefinition(schema);
graphQLInfo.setGraphQLSchema(schemaObj);
validationResponse.setGraphQLInfo(graphQLInfo);
schema = retrieveGraphQLSchemaFromURL(url);
}
} else {
} else if (filename == null) {
throw new APIManagementException("GraphQL filename cannot be null",
ExceptionCodes.INVALID_GRAPHQL_FILE);
} else if (!filename.endsWith(".graphql") && !filename.endsWith(".txt") && !filename.endsWith(".sdl")) {
throw new APIManagementException("Unsupported extension type of file: " + filename,
ExceptionCodes.UNSUPPORTED_GRAPHQL_FILE_EXTENSION);
}

if (schema == null || schema.isEmpty()) {
throw new APIManagementException("GraphQL Schema cannot be empty or null to validate it",
ExceptionCodes.GRAPHQL_SCHEMA_CANNOT_BE_NULL);
}

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

if (validationErrors.toArray().length > 0) {
errorMessage = "InValid Schema";
validationResponse.isValid(Boolean.FALSE);
validationResponse.errorMessage(errorMessage);
} else {
validationResponse.setIsValid(Boolean.TRUE);
GraphQLValidationResponseGraphQLInfoDTO graphQLInfo = new GraphQLValidationResponseGraphQLInfoDTO();
GraphQLSchemaDefinition graphql = new GraphQLSchemaDefinition();
List<URITemplate> operationList = graphql.extractGraphQLOperationList(typeRegistry, null);
List<APIOperationsDTO> operationArray = APIMappingUtil.fromURITemplateListToOprationList(operationList);
graphQLInfo.setOperations(operationArray);
GraphQLSchemaDTO schemaObj = new GraphQLSchemaDTO();
schemaObj.setSchemaDefinition(schema);
graphQLInfo.setGraphQLSchema(schemaObj);
validationResponse.setGraphQLInfo(graphQLInfo);
}
isValid = validationResponse.isIsValid();
errorMessage = validationResponse.getErrorMessage();
} catch (SchemaProblem e) {
Expand All @@ -2054,6 +2080,80 @@ public static GraphQLValidationResponseDTO validateGraphQLSchema(String filename
return validationResponse;
}

/**
* Generate the GraphQL schema by performing an introspection query on the provided endpoint.
*
* @param url The URL of the GraphQL endpoint to perform the introspection query on.
* @return The GraphQL schema as a string.
* @throws APIManagementException If an error occurs during the schema generation process
*/
public static String generateGraphQLSchemaFromIntrospection(String url) throws APIManagementException {
String schema = null;
try {
URL urlObj = new URL(url);
HttpClient httpClient = APIUtil.getHttpClient(urlObj.getPort(), urlObj.getProtocol());
Gson gson = new Gson();

if (graphQLIntrospectionQuery == null || graphQLIntrospectionQuery.isEmpty()) {
graphQLIntrospectionQuery = APIUtil.getIntrospectionQuery();
}
String requestBody = gson.toJson(
JsonParser.parseString("{\"query\": \"" + graphQLIntrospectionQuery + "\"}"));

HttpPost httpPost = new HttpPost(url);
httpPost.setHeader("Content-Type", "application/json");
httpPost.setEntity(new StringEntity(requestBody));
HttpResponse response = httpClient.execute(httpPost);

if (HttpStatus.SC_OK == response.getStatusLine().getStatusCode()) {
String schemaResponse = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
Type type = new TypeToken<Map<String, Object>>() {
}.getType();
Map<String, Object> schemaMap = gson.fromJson(schemaResponse, type);
Document schemaDocument = new IntrospectionResultToSchema().createSchemaDefinition(
(Map<String, Object>) schemaMap.get("data"));
schema = AstPrinter.printAst(schemaDocument);
} else {
log.error("Unable to generate GraphQL schema from introspection. URL."
+ " Returned response code: " + response.getStatusLine().getStatusCode()
+ " from endpoint: " + url);
throw new APIManagementException("Error occurred while generating GraphQL schema from introspection",
ExceptionCodes.GENERATE_GRAPHQL_SCHEMA_FROM_INTROSPECTION_ERROR);
}
} catch (Exception e) {
log.error("Exception occurred while generating GraphQL schema from introspection. URL: " + url
+ ". Exception: " + e.getMessage(), e);
throw new APIManagementException("Error occurred while generating GraphQL schema from introspection",
ExceptionCodes.GENERATE_GRAPHQL_SCHEMA_FROM_INTROSPECTION_ERROR);
}
return schema;
}

/**
* Retrieve the GraphQL schema from the specified URL.
*
* @param url The URL of the GraphQL schema to retrieve.
* @return The GraphQL schema as a string.
* @throws APIManagementException If an error occurs while retrieving the schema
*/
public static String retrieveGraphQLSchemaFromURL(String url) throws APIManagementException {
String schema = null;
try {
URL urlObj = new URL(url);
HttpClient httpClient = APIUtil.getHttpClient(urlObj.getPort(), urlObj.getProtocol());
HttpGet httpGet = new HttpGet(url);
HttpResponse response = httpClient.execute(httpGet);

if (HttpStatus.SC_OK == response.getStatusLine().getStatusCode()) {
schema = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
}
} catch (IOException e) {
throw new APIManagementException("Error occurred while retrieving GraphQL schema from schema URL",
ExceptionCodes.RETRIEVE_GRAPHQL_SCHEMA_FROM_URL_ERROR);
}
return schema;
}

/**
* Update thumbnail of an API/API Product
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1518,8 +1518,8 @@ public Response importAsyncAPISpecification( @Multipart(value = "file", required
@ApiResponse(code = 201, message = "Created. Successful response with the newly created object as entity in the body. Location header contains URL of newly created entity. ", response = APIDTO.class),
@ApiResponse(code = 400, message = "Bad Request. Invalid request or validation error.", response = ErrorDTO.class),
@ApiResponse(code = 415, message = "Unsupported Media Type. The entity of the request was not in a supported format.", response = ErrorDTO.class) })
public Response importGraphQLSchema( @ApiParam(value = "Validator for conditional requests; based on ETag. " )@HeaderParam("If-Match") String ifMatch, @Multipart(value = "type", required = false) String type, @Multipart(value = "file", required = false) InputStream fileInputStream, @Multipart(value = "file" , required = false) Attachment fileDetail, @Multipart(value = "additionalProperties", required = false) String additionalProperties) throws APIManagementException{
return delegate.importGraphQLSchema(ifMatch, type, fileInputStream, fileDetail, additionalProperties, securityContext);
public Response importGraphQLSchema( @ApiParam(value = "Validator for conditional requests; based on ETag. " )@HeaderParam("If-Match") String ifMatch, @Multipart(value = "type", required = false) String type, @Multipart(value = "file", required = false) InputStream fileInputStream, @Multipart(value = "file" , required = false) Attachment fileDetail, @Multipart(value = "url", required = false) String url, @Multipart(value = "schema", required = false) String schema, @Multipart(value = "additionalProperties", required = false) String additionalProperties) throws APIManagementException{
return delegate.importGraphQLSchema(ifMatch, type, fileInputStream, fileDetail, url, schema, additionalProperties, securityContext);
}

@POST
Expand Down Expand Up @@ -2018,8 +2018,8 @@ public Response validateEndpoint( @NotNull @ApiParam(value = "API endpoint url",
@ApiResponse(code = 200, message = "OK. API definition validation information is returned ", response = GraphQLValidationResponseDTO.class),
@ApiResponse(code = 400, message = "Bad Request. Invalid request or validation error.", response = ErrorDTO.class),
@ApiResponse(code = 404, message = "Not Found. The specified resource does not exist.", response = ErrorDTO.class) })
public Response validateGraphQLSchema( @Multipart(value = "file") InputStream fileInputStream, @Multipart(value = "file" ) Attachment fileDetail) throws APIManagementException{
return delegate.validateGraphQLSchema(fileInputStream, fileDetail, securityContext);
public Response validateGraphQLSchema( @ApiParam(value = "Specify whether to use Introspection to obtain the GraphQL Schema ", defaultValue="false") @DefaultValue("false") @QueryParam("useIntrospection") Boolean useIntrospection, @Multipart(value = "file", required = false) InputStream fileInputStream, @Multipart(value = "file" , required = false) Attachment fileDetail, @Multipart(value = "url", required = false) String url) throws APIManagementException{
return delegate.validateGraphQLSchema(useIntrospection, fileInputStream, fileDetail, url, securityContext);
}

@POST
Expand Down
Loading

0 comments on commit 33f68da

Please sign in to comment.