├── .gitattributes ├── images ├── field-paths.png ├── requests-list.png ├── schema-fields.png ├── operation-output.png ├── operations-list.png ├── request-output.png ├── schema-field-file.png ├── default-param-output.png ├── schema-fields-depth.png └── unique-field-output.png ├── src ├── main │ ├── resources │ │ ├── META-INF │ │ │ └── native-image │ │ │ │ ├── resource-config.json │ │ │ │ ├── jni-config.json │ │ │ │ ├── reflect-config.json │ │ │ │ └── native-image.properties │ │ ├── application.properties │ │ ├── parseDocs.js │ │ ├── gql-docs-visitor.js │ │ ├── gql-strings-visitor.js │ │ └── walk.js │ └── java │ │ └── com │ │ └── pdstat │ │ └── gqlextractor │ │ ├── service │ │ ├── ResourceService.java │ │ ├── GqlOperationFilesWriterService.java │ │ ├── GqlFieldWordListWriterService.java │ │ ├── GqlJsonRequestFileWriterService.java │ │ ├── GqlSchemaFieldPathWriterService.java │ │ ├── GqlFieldPathWriterService.java │ │ ├── GqlPathFinder.java │ │ ├── GqlRequestFactoryService.java │ │ ├── GqlSchemaPathFinder.java │ │ ├── GqlMergerService.java │ │ └── GqlExtractorOutputHandlerService.java │ │ ├── config │ │ └── JacksonConfig.java │ │ ├── model │ │ ├── GqlRequest.java │ │ └── OutputMode.java │ │ ├── Constants.java │ │ ├── repo │ │ ├── DefaultParamsRepository.java │ │ ├── GqlFieldRepository.java │ │ ├── GqlFragmentDefinitionsRepository.java │ │ ├── GqlOperationsRepository.java │ │ ├── GqlDocumentRepository.java │ │ └── GqlSchemaRepository.java │ │ ├── GqlExtractor.java │ │ ├── graal │ │ └── GraalAcornWalker.java │ │ ├── extractor │ │ └── GqlDocumentExtractor.java │ │ └── deserialiser │ │ └── DocumentDeserialiser.java └── test │ └── java │ └── com │ └── pdstat │ └── gqlextractor │ ├── repo │ ├── GqlSchemaRepositorySchemaTest.java │ ├── GqlFragmentDefinitionsRepositoryTest.java │ ├── GqlFieldRepositoryTest.java │ └── GqlOperationsRepositoryTest.java │ ├── service │ ├── GqlPathFinderTest.java │ ├── GqlMergerServiceTest.java │ ├── GqlSchemaPathFinderTest.java │ └── GqlRequestFactoryServiceTest.java │ └── model │ └── GqlRequestTest.java ├── .gitignore ├── .mvn └── wrapper │ └── maven-wrapper.properties ├── pom.xml ├── mvnw.cmd ├── mvnw └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | /mvnw text eol=lf 2 | *.cmd text eol=crlf 3 | -------------------------------------------------------------------------------- /images/field-paths.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdstat/graphqlextractor/HEAD/images/field-paths.png -------------------------------------------------------------------------------- /images/requests-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdstat/graphqlextractor/HEAD/images/requests-list.png -------------------------------------------------------------------------------- /images/schema-fields.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdstat/graphqlextractor/HEAD/images/schema-fields.png -------------------------------------------------------------------------------- /images/operation-output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdstat/graphqlextractor/HEAD/images/operation-output.png -------------------------------------------------------------------------------- /images/operations-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdstat/graphqlextractor/HEAD/images/operations-list.png -------------------------------------------------------------------------------- /images/request-output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdstat/graphqlextractor/HEAD/images/request-output.png -------------------------------------------------------------------------------- /images/schema-field-file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdstat/graphqlextractor/HEAD/images/schema-field-file.png -------------------------------------------------------------------------------- /images/default-param-output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdstat/graphqlextractor/HEAD/images/default-param-output.png -------------------------------------------------------------------------------- /images/schema-fields-depth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdstat/graphqlextractor/HEAD/images/schema-fields-depth.png -------------------------------------------------------------------------------- /images/unique-field-output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdstat/graphqlextractor/HEAD/images/unique-field-output.png -------------------------------------------------------------------------------- /src/main/resources/META-INF/native-image/resource-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "resources": { 3 | "includes": [ 4 | { 5 | "pattern": ".*\\.js$" 6 | } 7 | ] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/native-image/jni-config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name":"sun.net.dns.ResolverConfigurationImpl", 4 | "fields":[ 5 | {"name":"os_searchlist"}, 6 | {"name":"os_nameservers"} 7 | ] 8 | } 9 | ] -------------------------------------------------------------------------------- /src/main/resources/META-INF/native-image/reflect-config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "com.pdstat.gqlextractor.model.GqlRequest", 4 | "allDeclaredFields": true, 5 | "allDeclaredMethods": true, 6 | "allDeclaredConstructors": true 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/native-image/native-image.properties: -------------------------------------------------------------------------------- 1 | Args = --allow-incomplete-classpath \ 2 | -H:+AllowDeprecatedBuilderClassesOnImageClasspath \ 3 | --language:js \ 4 | --report-unsupported-elements-at-runtime \ 5 | --initialize-at-run-time=sun.net.dns.ResolverConfigurationImpl 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | logging.level.root=ERROR 2 | logging.level.org.springframework=ERROR 3 | logging.level.com.pdstat=INFO 4 | logging.pattern.console=%msg%n 5 | spring.codec.max-in-memory-size=10MB 6 | spring.application.name=GQL Extractor 7 | spring.main.banner-mode=off 8 | spring.main.web-application-type=NONE -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | -------------------------------------------------------------------------------- /src/main/java/com/pdstat/gqlextractor/service/ResourceService.java: -------------------------------------------------------------------------------- 1 | package com.pdstat.gqlextractor.service; 2 | 3 | import org.springframework.core.io.Resource; 4 | import org.springframework.stereotype.Service; 5 | import org.springframework.util.FileCopyUtils; 6 | 7 | import java.io.IOException; 8 | import java.io.InputStreamReader; 9 | import java.io.Reader; 10 | 11 | import static java.nio.charset.StandardCharsets.UTF_8; 12 | 13 | @Service 14 | public class ResourceService { 15 | 16 | public String readResourceFileContent(Resource resource) throws IOException { 17 | try (Reader reader = new InputStreamReader(resource.getInputStream(), UTF_8)) { 18 | return FileCopyUtils.copyToString(reader); 19 | } 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/pdstat/gqlextractor/config/JacksonConfig.java: -------------------------------------------------------------------------------- 1 | package com.pdstat.gqlextractor.config; 2 | 3 | import com.pdstat.gqlextractor.deserialiser.DocumentDeserialiser; 4 | import graphql.language.Document; 5 | 6 | import com.fasterxml.jackson.databind.ObjectMapper; 7 | import com.fasterxml.jackson.databind.module.SimpleModule; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | 11 | @Configuration 12 | public class JacksonConfig { 13 | 14 | @Bean 15 | public ObjectMapper objectMapper() { 16 | ObjectMapper objectMapper = new ObjectMapper(); 17 | SimpleModule module = new SimpleModule(); 18 | module.addDeserializer(Document.class, new DocumentDeserialiser()); 19 | objectMapper.registerModule(module); 20 | return objectMapper; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/pdstat/gqlextractor/model/GqlRequest.java: -------------------------------------------------------------------------------- 1 | package com.pdstat.gqlextractor.model; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | public class GqlRequest { 7 | 8 | private String operationName; 9 | private final Map variables = new HashMap<>(); 10 | private String query; 11 | 12 | public String getQuery() { 13 | return query; 14 | } 15 | 16 | public String getOperationName() { 17 | return operationName; 18 | } 19 | 20 | public void setOperationName(String operationName) { 21 | this.operationName = operationName; 22 | } 23 | 24 | public void setQuery(String query) { 25 | this.query = query; 26 | } 27 | 28 | public Map getVariables() { 29 | return variables; 30 | } 31 | 32 | public void setVariable(String key, Object value) { 33 | variables.put(key, value); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | wrapperVersion=3.3.2 18 | distributionType=only-script 19 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip 20 | -------------------------------------------------------------------------------- /src/main/resources/parseDocs.js: -------------------------------------------------------------------------------- 1 | extractObjectFromAST = (node) => { 2 | if (node.type !== "ObjectExpression") return null; 3 | 4 | const obj = {}; 5 | node.properties.forEach(prop => { 6 | if (!prop.key || !prop.value) return; 7 | 8 | const key = prop.key.name || prop.key.value; 9 | let value; 10 | 11 | switch (prop.value.type) { 12 | case "Literal": 13 | value = prop.value.value; 14 | break; 15 | case "ArrayExpression": 16 | value = prop.value.elements.map(el => extractObjectFromAST(el) || (el.value ?? null)); 17 | break; 18 | case "ObjectExpression": 19 | value = extractObjectFromAST(prop.value); 20 | break; 21 | } 22 | 23 | obj[key] = value; 24 | }); 25 | 26 | return obj; 27 | } 28 | 29 | parseJavascript = (ast, visitors) => { 30 | try { 31 | acorn.walk.simple(ast, visitors); 32 | return JSON.stringify(matchedObjects); 33 | } catch (error) { 34 | console.error(error); 35 | return JSON.stringify([]); 36 | } 37 | } -------------------------------------------------------------------------------- /src/main/java/com/pdstat/gqlextractor/model/OutputMode.java: -------------------------------------------------------------------------------- 1 | package com.pdstat.gqlextractor.model; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | public enum OutputMode { 7 | 8 | OPERATIONS("operations"), 9 | FIELDS("fields"), 10 | PATHS("paths"), 11 | ALL("all"), 12 | REQUESTS("requests"); 13 | 14 | private static final Map BY_MODE = new HashMap<>(); 15 | 16 | static { 17 | BY_MODE.put(OPERATIONS.mode, OPERATIONS); 18 | BY_MODE.put(ALL.mode, ALL); 19 | BY_MODE.put(REQUESTS.mode, REQUESTS); 20 | BY_MODE.put(FIELDS.mode, FIELDS); 21 | BY_MODE.put(PATHS.mode, PATHS); 22 | } 23 | 24 | private final String mode; 25 | 26 | OutputMode(String mode) { 27 | this.mode = mode; 28 | } 29 | 30 | public static OutputMode fromMode(String mode) { 31 | OutputMode outputMode = BY_MODE.get(mode); 32 | if (outputMode == null) { 33 | return REQUESTS; 34 | } 35 | return outputMode; 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/resources/gql-docs-visitor.js: -------------------------------------------------------------------------------- 1 | ({ 2 | VariableDeclarator: (node) => { 3 | if (node.init && node.init.type === 4 | "ObjectExpression") { 5 | const kindProp = node.init.properties.find(prop => prop.key?.name === 6 | "kind" && prop.value?.value === "Document"); 7 | const definitionsProp = 8 | node.init.properties.find(prop => prop.key?.name === "definitions" && prop.value?.type === 9 | "ArrayExpression"); 10 | if (kindProp && definitionsProp) { 11 | const extractedObject = 12 | extractObjectFromAST(node.init); 13 | if (extractedObject) matchedObjects.push(extractedObject); 14 | } 15 | } 16 | }, 17 | Literal: (node) => { 18 | if (node.value && typeof node.value === 'string' && 19 | (node.value.indexOf('"kind"') > -1) && (node.value.indexOf('"Document"') > -1) && 20 | (node.value.indexOf('"definitions"') > -1)) { 21 | matchedObjects.push(node.value); 22 | } 23 | } 24 | }) -------------------------------------------------------------------------------- /src/test/java/com/pdstat/gqlextractor/repo/GqlSchemaRepositorySchemaTest.java: -------------------------------------------------------------------------------- 1 | package com.pdstat.gqlextractor.repo; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.pdstat.gqlextractor.Constants; 5 | import graphql.language.AstPrinter; 6 | import graphql.language.Document; 7 | import org.junit.jupiter.api.Test; 8 | import org.junit.jupiter.api.extension.ExtendWith; 9 | import org.mockito.InjectMocks; 10 | import org.mockito.Mock; 11 | import org.mockito.Mockito; 12 | import org.mockito.Spy; 13 | import org.mockito.junit.jupiter.MockitoExtension; 14 | import org.springframework.boot.ApplicationArguments; 15 | 16 | import java.util.List; 17 | 18 | @ExtendWith(MockitoExtension.class) 19 | public class GqlSchemaRepositorySchemaTest { 20 | 21 | @Spy 22 | private ObjectMapper mapper = new ObjectMapper(); 23 | 24 | @Mock 25 | private ApplicationArguments appArgs; 26 | 27 | @InjectMocks 28 | private GqlSchemaRepository gqlSchemaRepository; 29 | 30 | // @Test 31 | // void testGetGqlSchema() { 32 | // Mockito.when(appArgs.containsOption(Constants.Arguments.REQUEST_HEADER)).thenReturn(false); 33 | // Mockito.when(appArgs.getOptionValues(Constants.Arguments.REQUEST_HEADER)) 34 | // .thenReturn(List.of("X-Airbnb-Api-Key: d306zoyjsyarp7ifhu67rjxn52tv0t20")); 35 | // Document schema = gqlSchemaRepository.getGqlSchema("https://rickandmortyapi.com/graphql"); 36 | // System.out.println(AstPrinter.printAst(schema)); 37 | // } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/resources/gql-strings-visitor.js: -------------------------------------------------------------------------------- 1 | ({ 2 | TemplateLiteral: (node) => { 3 | if (node.quasis && 4 | node.quasis.length > 0) { 5 | for (let i = 0; i < node.quasis.length; i++) { 6 | const quasisVal = node.quasis[i].value; 7 | if ((typeof quasisVal.raw === 'string' && 8 | quasisVal.raw.indexOf('{') > -1) && ( 9 | quasisVal.raw.indexOf('}') > -1) && 10 | (quasisVal.raw.indexOf('query ') > -1 || 11 | quasisVal.raw.indexOf('mutation ') > - 12 | 1 || 13 | quasisVal.raw.indexOf('fragment ') > - 14 | 1 || quasisVal.raw.indexOf( 15 | 'subscription ') > -1)) { 16 | const rawQuasisVal = quasisVal.raw.trim() 17 | .replaceAll('\n', '').replaceAll(/\$\{[^}]+\}/g, ''); 18 | matchedObjects.push(rawQuasisVal); 19 | } 20 | } 21 | } 22 | }, 23 | Literal: (node) => { 24 | if (node.value && typeof node.value === 'string' && ( 25 | node.value.indexOf('{') > -1) && (node.value 26 | .indexOf('}') > -1) && (node.value.indexOf( 27 | 'query ') > -1 || node.value.indexOf( 28 | 'mutation ') > -1 || node.value.indexOf( 29 | 'fragment ') > -1 || node.value.indexOf( 30 | 'subscription ') > -1)) { 31 | matchedObjects.push(node.value.trim()); 32 | } 33 | } 34 | }) -------------------------------------------------------------------------------- /src/main/java/com/pdstat/gqlextractor/Constants.java: -------------------------------------------------------------------------------- 1 | package com.pdstat.gqlextractor; 2 | 3 | public class Constants { 4 | 5 | public interface Gql { 6 | 7 | interface Scalar { 8 | String STRING = "String"; 9 | String INT = "Int"; 10 | String FLOAT = "Float"; 11 | String BOOLEAN = "Boolean"; 12 | String LONG = "Long"; 13 | String ID = "ID"; 14 | } 15 | 16 | } 17 | 18 | public interface Arguments { 19 | String HELP = "help"; 20 | String INPUT_URLS = "input-urls"; 21 | String DEFAULT_PARAMS = "default-params"; 22 | String INPUT_DIRECTORY = "input-directory"; 23 | String INPUT_SCHEMA = "input-schema"; 24 | String INPUT_OPERATIONS = "input-operations"; 25 | String REQUEST_HEADER = "request-header"; 26 | String OUTPUT_DIRECTORY = "output-directory"; 27 | String SEARCH_FIELD = "search-field"; 28 | String DEPTH = "depth"; 29 | String OUTPUT_MODE = "output-mode"; 30 | } 31 | 32 | public interface Output { 33 | 34 | interface FILES { 35 | String FIELDS_FILE = "unique-fields.txt"; 36 | String FIELD_PATHS = "-paths.txt"; 37 | } 38 | 39 | interface DIRECTORIES { 40 | String REQUESTS = "requests"; 41 | String OPERATIONS = "operations"; 42 | String FIELD_PATHS = "field-paths"; 43 | String SCHEMA_FIELD_PATHS = "schema-field-paths"; 44 | String WORDLIST = "wordlist"; 45 | } 46 | 47 | } 48 | 49 | private Constants() { 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/pdstat/gqlextractor/service/GqlOperationFilesWriterService.java: -------------------------------------------------------------------------------- 1 | package com.pdstat.gqlextractor.service; 2 | 3 | import com.pdstat.gqlextractor.Constants; 4 | import com.pdstat.gqlextractor.repo.GqlOperationsRepository; 5 | import graphql.language.AstPrinter; 6 | import graphql.language.Document; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.stereotype.Service; 10 | 11 | import java.nio.file.Files; 12 | import java.nio.file.Path; 13 | import java.nio.file.Paths; 14 | import java.util.Map; 15 | 16 | @Service 17 | public class GqlOperationFilesWriterService { 18 | 19 | private static final Logger logger = LoggerFactory.getLogger(GqlOperationFilesWriterService.class); 20 | 21 | private final GqlOperationsRepository gqlOperationsRepository; 22 | 23 | public GqlOperationFilesWriterService(GqlOperationsRepository gqlOperationsRepository) { 24 | this.gqlOperationsRepository = gqlOperationsRepository; 25 | } 26 | 27 | public void writeOperationFiles(String outputDirectory) { 28 | for (Map.Entry entry : gqlOperationsRepository.getGqlOperations().entrySet()) { 29 | String gqlFileName = outputDirectory + "/" + Constants.Output.DIRECTORIES.OPERATIONS + "/" 30 | + entry.getKey() + ".graphql"; 31 | Path gqlFilePath = Paths.get(gqlFileName); 32 | logger.info("Writing operation file: {}", gqlFilePath.getFileName()); 33 | try { 34 | Files.createDirectories(gqlFilePath.getParent()); 35 | Files.write(gqlFilePath, AstPrinter.printAst(entry.getValue()).getBytes()); 36 | } catch (Exception e) { 37 | logger.error("Error writing operation file: {}", gqlFilePath.getFileName(), e); 38 | } 39 | } 40 | 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/pdstat/gqlextractor/service/GqlFieldWordListWriterService.java: -------------------------------------------------------------------------------- 1 | package com.pdstat.gqlextractor.service; 2 | 3 | import com.pdstat.gqlextractor.Constants; 4 | import com.pdstat.gqlextractor.repo.GqlFieldRepository; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.stereotype.Service; 8 | 9 | import java.io.BufferedWriter; 10 | import java.io.IOException; 11 | import java.nio.file.Files; 12 | import java.nio.file.Path; 13 | import java.nio.file.Paths; 14 | import java.util.List; 15 | 16 | @Service 17 | public class GqlFieldWordListWriterService { 18 | 19 | private static final Logger logger = LoggerFactory.getLogger(GqlFieldWordListWriterService.class); 20 | 21 | private final GqlFieldRepository gqlFieldRepository; 22 | 23 | public GqlFieldWordListWriterService(GqlFieldRepository gqlFieldRepository) { 24 | this.gqlFieldRepository = gqlFieldRepository; 25 | } 26 | 27 | public void writeFieldsFile(String outputDirectory) { 28 | logger.info("Writing GQL unique fields file"); 29 | String fieldsFileName = outputDirectory + "/" + Constants.Output.DIRECTORIES.WORDLIST + "/" 30 | + Constants.Output.FILES.FIELDS_FILE; 31 | Path fieldsFilePath = Paths.get(fieldsFileName); 32 | List gqlFields = this.gqlFieldRepository.getGqlFields(); 33 | try { 34 | Files.createDirectories(fieldsFilePath.getParent()); 35 | try (BufferedWriter writer = Files.newBufferedWriter(fieldsFilePath)) { 36 | for (String field : gqlFields) { 37 | writer.write(field); 38 | writer.newLine(); 39 | } 40 | } 41 | } catch (IOException e) { 42 | logger.error("Error writing fields file: {}", fieldsFileName, e); 43 | } 44 | 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/pdstat/gqlextractor/repo/DefaultParamsRepository.java: -------------------------------------------------------------------------------- 1 | package com.pdstat.gqlextractor.repo; 2 | 3 | 4 | import com.fasterxml.jackson.core.type.TypeReference; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import com.pdstat.gqlextractor.Constants; 7 | import jakarta.annotation.PostConstruct; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.springframework.boot.ApplicationArguments; 11 | import org.springframework.stereotype.Repository; 12 | 13 | import java.io.IOException; 14 | import java.nio.file.Path; 15 | import java.nio.file.Paths; 16 | import java.util.HashMap; 17 | import java.util.Map; 18 | 19 | @Repository 20 | public class DefaultParamsRepository { 21 | 22 | private static final Logger logger = LoggerFactory.getLogger(DefaultParamsRepository.class); 23 | private Map defaultParams; 24 | private final ObjectMapper mapper; 25 | private final ApplicationArguments appArgs; 26 | 27 | public DefaultParamsRepository(ObjectMapper mapper, ApplicationArguments appArgs) { 28 | this.mapper = mapper; 29 | this.appArgs = appArgs; 30 | } 31 | 32 | @PostConstruct 33 | void init() { 34 | if (appArgs.containsOption(Constants.Arguments.DEFAULT_PARAMS)) { 35 | logger.info("Initializing default params repository"); 36 | String defaultParamsFile = appArgs.getOptionValues(Constants.Arguments.DEFAULT_PARAMS).get(0); 37 | Path defaultParamsPath = Paths.get(defaultParamsFile); 38 | TypeReference> typeRef = new TypeReference>() { 39 | }; 40 | try { 41 | defaultParams = mapper.readValue(defaultParamsPath.toFile(), typeRef); 42 | } catch (IOException e) { 43 | logger.error("Error reading default params file, skipping", e); 44 | } 45 | } 46 | } 47 | 48 | public Object getDefaultParam(String paramName) { 49 | if (defaultParams == null) { 50 | return null; 51 | } 52 | return defaultParams.get(paramName); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/test/java/com/pdstat/gqlextractor/service/GqlPathFinderTest.java: -------------------------------------------------------------------------------- 1 | package com.pdstat.gqlextractor.service; 2 | 3 | import graphql.language.Document; 4 | import graphql.parser.Parser; 5 | import org.junit.jupiter.api.Assertions; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import java.util.List; 9 | 10 | public class GqlPathFinderTest { 11 | 12 | @Test 13 | void testFindFieldPaths() { 14 | String getSupplyQuery = "query GetSupply($input: SupplyInput) { supply(input: $input) { stations { stationId " + 15 | "stationName location { lat lng } bikesAvailable bikeDocksAvailable ebikesAvailable scootersAvailable " + 16 | "totalBikesAvailable totalRideablesAvailable isValet isOffline isLightweight notices { ...NoticeFields }" + 17 | "siteId ebikes { rideableName batteryStatus { distanceRemaining { value unit } percent } } scooters { " + 18 | "rideableName batteryStatus { distanceRemaining { value unit } percent } } lastUpdatedMs } rideables { " + 19 | "rideableId location { lat lng } rideableType photoUrl batteryStatus { distanceRemaining { value unit } " + 20 | "percent } } notices { ...NoticeFields } requestErrors { ...NoticeFields } } } fragment NoticeFields on " + 21 | "Notice { localizedTitle localizedDescription url }"; 22 | Document getSupplyDocument = new Parser().parseDocument(getSupplyQuery); 23 | 24 | GqlPathFinder gqlPathFinder = new GqlPathFinder(); 25 | List fieldPaths = gqlPathFinder.findFieldPaths(getSupplyDocument, "distanceRemaining"); 26 | Assertions.assertNotNull(fieldPaths); 27 | Assertions.assertFalse(fieldPaths.isEmpty()); 28 | Assertions.assertEquals(3, fieldPaths.size()); 29 | Assertions.assertTrue(fieldPaths.contains("distanceRemaining -> batteryStatus -> ebikes -> stations -> supply -> GetSupply")); 30 | Assertions.assertTrue(fieldPaths.contains("distanceRemaining -> batteryStatus -> scooters -> stations -> supply -> GetSupply")); 31 | Assertions.assertTrue(fieldPaths.contains("distanceRemaining -> batteryStatus -> rideables -> supply -> GetSupply")); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/pdstat/gqlextractor/repo/GqlFieldRepository.java: -------------------------------------------------------------------------------- 1 | package com.pdstat.gqlextractor.repo; 2 | 3 | import graphql.language.Definition; 4 | import graphql.language.Field; 5 | import graphql.language.SelectionSet; 6 | import graphql.language.SelectionSetContainer; 7 | import graphql.language.Document; 8 | import jakarta.annotation.PostConstruct; 9 | import org.springframework.stereotype.Repository; 10 | 11 | import java.util.Set; 12 | import java.util.HashSet; 13 | import java.util.List; 14 | import java.util.Collections; 15 | import java.util.ArrayList; 16 | 17 | @Repository 18 | public class GqlFieldRepository { 19 | 20 | private final Set gqlFields = new HashSet<>(); 21 | 22 | private final GqlDocumentRepository gqlDocumentRepository; 23 | 24 | public GqlFieldRepository(GqlDocumentRepository gqlDocumentRepository) { 25 | this.gqlDocumentRepository = gqlDocumentRepository; 26 | } 27 | 28 | @PostConstruct 29 | void initGqlFields() { 30 | for (Document document : gqlDocumentRepository.getGqlDocuments()) { 31 | for (Definition definition : document.getDefinitions()) { 32 | if (definition instanceof SelectionSetContainer selectionSetContainer) { 33 | gqlFields.addAll(collectSelectionSetFields(selectionSetContainer)); 34 | } 35 | } 36 | } 37 | } 38 | 39 | public List getGqlFields() { 40 | List sortedGqlFields = new ArrayList<>(gqlFields); 41 | Collections.sort(sortedGqlFields); 42 | return sortedGqlFields; 43 | } 44 | 45 | private Set collectSelectionSetFields(SelectionSetContainer selectionSetContainer) { 46 | Set fields = new HashSet<>(); 47 | SelectionSet selectionSet = selectionSetContainer.getSelectionSet(); 48 | if (selectionSet != null) { 49 | selectionSet.getSelections().forEach(selection -> { 50 | if (selection instanceof Field field) { 51 | fields.add(field.getName()); 52 | fields.addAll(collectSelectionSetFields(field)); 53 | } 54 | }); 55 | } 56 | 57 | return fields; 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/pdstat/gqlextractor/service/GqlJsonRequestFileWriterService.java: -------------------------------------------------------------------------------- 1 | package com.pdstat.gqlextractor.service; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.pdstat.gqlextractor.Constants; 5 | import com.pdstat.gqlextractor.model.GqlRequest; 6 | import com.pdstat.gqlextractor.repo.GqlOperationsRepository; 7 | import graphql.language.Document; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.springframework.stereotype.Service; 11 | 12 | import java.io.IOException; 13 | import java.nio.file.Files; 14 | import java.nio.file.Path; 15 | import java.nio.file.Paths; 16 | 17 | @Service 18 | public class GqlJsonRequestFileWriterService { 19 | 20 | private static final Logger logger = LoggerFactory.getLogger(GqlJsonRequestFileWriterService.class); 21 | 22 | private final GqlOperationsRepository gqlOperationsRepository; 23 | private final GqlRequestFactoryService gqlRequestFactoryService; 24 | private final ObjectMapper mapper; 25 | 26 | public GqlJsonRequestFileWriterService(GqlOperationsRepository gqlOperationsRepository, 27 | GqlRequestFactoryService gqlRequestFactoryService, 28 | ObjectMapper mapper) { 29 | this.gqlOperationsRepository = gqlOperationsRepository; 30 | this.gqlRequestFactoryService = gqlRequestFactoryService; 31 | this.mapper = mapper; 32 | } 33 | 34 | public void writeJsonRequestFiles(String outputDirectory) { 35 | for (Document document : gqlOperationsRepository.getGqlOperations().values()) { 36 | GqlRequest gqlRequest = gqlRequestFactoryService.createGqlRequest(document); 37 | String jsonFileName = outputDirectory + "/" + Constants.Output.DIRECTORIES.REQUESTS + "/" 38 | + gqlRequest.getOperationName() + ".json"; 39 | Path jsonFilePath = Paths.get(jsonFileName); 40 | logger.info("Writing json request file: {}", jsonFilePath.getFileName()); 41 | try { 42 | Files.createDirectories(jsonFilePath.getParent()); 43 | Files.write(jsonFilePath, mapper.writeValueAsString(gqlRequest).getBytes()); 44 | } catch (IOException e) { 45 | logger.error("Error writing json request file: {}", jsonFilePath.getFileName(), e); 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/pdstat/gqlextractor/service/GqlSchemaFieldPathWriterService.java: -------------------------------------------------------------------------------- 1 | package com.pdstat.gqlextractor.service; 2 | 3 | import com.pdstat.gqlextractor.Constants; 4 | import com.pdstat.gqlextractor.repo.GqlSchemaRepository; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.stereotype.Service; 8 | 9 | import java.io.BufferedWriter; 10 | import java.io.IOException; 11 | import java.nio.file.Files; 12 | import java.nio.file.Path; 13 | import java.nio.file.Paths; 14 | import java.util.List; 15 | 16 | @Service 17 | public class GqlSchemaFieldPathWriterService { 18 | 19 | private static final Logger logger = LoggerFactory.getLogger(GqlSchemaFieldPathWriterService.class); 20 | 21 | private final GqlSchemaRepository gqlSchemaRepository; 22 | private final GqlSchemaPathFinder gqlSchemaPathFinder; 23 | 24 | public GqlSchemaFieldPathWriterService(GqlSchemaRepository gqlSchemaRepository, GqlSchemaPathFinder gqlSchemaPathFinder) { 25 | this.gqlSchemaRepository = gqlSchemaRepository; 26 | this.gqlSchemaPathFinder = gqlSchemaPathFinder; 27 | } 28 | 29 | public void writeSchemaFieldPaths(String outputDirectory, String inputSchema, String searchField, int maxDepth) { 30 | logger.info("Finding schema field paths for field: {}", searchField); 31 | String schemaFieldPathsFileName = outputDirectory + "/" + Constants.Output.DIRECTORIES.SCHEMA_FIELD_PATHS + "/" 32 | + searchField + Constants.Output.FILES.FIELD_PATHS; 33 | Path schemaFieldPathsFilePath = Paths.get(schemaFieldPathsFileName); 34 | List gqlSchemaFieldPaths = gqlSchemaPathFinder 35 | .findFieldPaths(gqlSchemaRepository.getGqlSchema(inputSchema), searchField, maxDepth); 36 | try { 37 | Files.createDirectories(schemaFieldPathsFilePath.getParent()); 38 | try (BufferedWriter writer = Files.newBufferedWriter(schemaFieldPathsFilePath)) { 39 | logger.info("Writing GQL schema fields report file"); 40 | for (String fieldPath : gqlSchemaFieldPaths) { 41 | logger.info(fieldPath); 42 | writer.write(fieldPath); 43 | writer.newLine(); 44 | } 45 | } 46 | } catch (IOException e) { 47 | logger.error("Error writing schema field paths file: {}", schemaFieldPathsFileName, e); 48 | } 49 | 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 3.3.5 9 | 10 | 11 | com.pdstat 12 | gqlextractor 13 | 1.0.0 14 | GraphQL Extractor 15 | GraphQL Extractor 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 17 31 | 32 | 33 | 34 | org.springframework.boot 35 | spring-boot-starter 36 | 37 | 38 | 39 | com.graphql-java 40 | graphql-java 41 | 22.3 42 | 43 | 44 | 45 | 46 | org.springframework.boot 47 | spring-boot-starter-webflux 48 | 49 | 50 | 51 | org.springframework.boot 52 | spring-boot-starter-json 53 | 54 | 55 | 56 | org.graalvm.js 57 | js 58 | 23.0.7 59 | pom 60 | 61 | 62 | 63 | 64 | org.graalvm.polyglot 65 | polyglot 66 | 23.1.6 67 | 68 | 69 | 70 | 71 | org.springframework.boot 72 | spring-boot-starter-test 73 | test 74 | 75 | 76 | 77 | 78 | 79 | 80 | org.springframework.boot 81 | spring-boot-maven-plugin 82 | 83 | 84 | org.graalvm.buildtools 85 | native-maven-plugin 86 | 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /src/main/java/com/pdstat/gqlextractor/service/GqlFieldPathWriterService.java: -------------------------------------------------------------------------------- 1 | package com.pdstat.gqlextractor.service; 2 | 3 | import com.pdstat.gqlextractor.Constants; 4 | import com.pdstat.gqlextractor.repo.GqlOperationsRepository; 5 | import graphql.language.Document; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.stereotype.Service; 9 | 10 | import java.io.BufferedWriter; 11 | import java.io.IOException; 12 | import java.nio.file.Files; 13 | import java.nio.file.Path; 14 | import java.nio.file.Paths; 15 | import java.util.*; 16 | 17 | @Service 18 | public class GqlFieldPathWriterService { 19 | 20 | private static final Logger logger = LoggerFactory.getLogger(GqlFieldPathWriterService.class); 21 | 22 | private final GqlOperationsRepository gqlOperationsRepository; 23 | private final GqlPathFinder gqlPathFinder; 24 | 25 | public GqlFieldPathWriterService(GqlOperationsRepository gqlOperationsRepository, 26 | GqlPathFinder gqlPathFinder) { 27 | this.gqlOperationsRepository = gqlOperationsRepository; 28 | this.gqlPathFinder = gqlPathFinder; 29 | } 30 | 31 | public void writeFieldsReport(String outputDirectory, String searchField) { 32 | logger.info("Finding field paths for field: {}", searchField); 33 | String fieldsFileName = outputDirectory + "/" + Constants.Output.DIRECTORIES.FIELD_PATHS + "/" + searchField + 34 | Constants.Output.FILES.FIELD_PATHS; 35 | Path fieldsFilePath = Paths.get(fieldsFileName); 36 | Set fieldPaths = new HashSet<>(); 37 | for (Document document : gqlOperationsRepository.getGqlOperations().values()) { 38 | fieldPaths.addAll(gqlPathFinder.findFieldPaths(document, searchField)); 39 | } 40 | List sortedFieldPaths = new ArrayList<>(fieldPaths); 41 | Collections.sort(sortedFieldPaths); 42 | 43 | try { 44 | Files.createDirectories(fieldsFilePath.getParent()); 45 | try (BufferedWriter writer = Files.newBufferedWriter(fieldsFilePath)) { 46 | logger.info("Writing GQL operations fields report file"); 47 | for (String field : sortedFieldPaths) { 48 | logger.info(field); 49 | writer.write(field); 50 | writer.newLine(); 51 | } 52 | } 53 | } catch (IOException e) { 54 | logger.error("Error writing field paths file: {}", fieldsFileName, e); 55 | } 56 | 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/pdstat/gqlextractor/repo/GqlFragmentDefinitionsRepository.java: -------------------------------------------------------------------------------- 1 | package com.pdstat.gqlextractor.repo; 2 | 3 | import com.pdstat.gqlextractor.service.GqlMergerService; 4 | import graphql.language.Definition; 5 | import graphql.language.Document; 6 | import graphql.language.FragmentDefinition; 7 | import jakarta.annotation.PostConstruct; 8 | import org.springframework.stereotype.Repository; 9 | 10 | import java.util.Map; 11 | import java.util.HashMap; 12 | 13 | @Repository 14 | public class GqlFragmentDefinitionsRepository { 15 | 16 | private final GqlDocumentRepository gqlDocumentRepository; 17 | private final GqlMergerService gqlMergerService; 18 | 19 | private final Map gqlFragmentDefinitions = new HashMap<>(); 20 | 21 | public GqlFragmentDefinitionsRepository(GqlDocumentRepository gqlDocumentRepository, GqlMergerService gqlMergerService) { 22 | this.gqlDocumentRepository = gqlDocumentRepository; 23 | this.gqlMergerService = gqlMergerService; 24 | } 25 | 26 | @PostConstruct 27 | void initFragmentDefinitions() { 28 | for (Document document : gqlDocumentRepository.getGqlDocuments()) { 29 | for (Definition definition : document.getDefinitions()) { 30 | if (definition instanceof FragmentDefinition fragmentDefinition) { 31 | String fragmentDefinitionName = fragmentDefinition.getName(); 32 | if (gqlFragmentDefinitions.containsKey(fragmentDefinitionName)) { 33 | FragmentDefinition storedFragmentDefinition = gqlFragmentDefinitions.get(fragmentDefinitionName); 34 | 35 | Document storedFragmentDocument = buildDocumentFromFragmentDefinition(storedFragmentDefinition); 36 | Document fragmentDocument = buildDocumentFromFragmentDefinition(fragmentDefinition); 37 | 38 | if (!documentsEqual(storedFragmentDocument, fragmentDocument)) { 39 | Document mergedDocument = gqlMergerService.mergeGraphQLDocuments(storedFragmentDocument, fragmentDocument); 40 | FragmentDefinition mergedFragmentDefinition = (FragmentDefinition) mergedDocument.getDefinitions().get(0); 41 | 42 | gqlFragmentDefinitions.put(fragmentDefinition.getName(), mergedFragmentDefinition); 43 | } 44 | 45 | } else { 46 | gqlFragmentDefinitions.put(fragmentDefinition.getName(), fragmentDefinition); 47 | } 48 | } 49 | } 50 | } 51 | } 52 | 53 | private Document buildDocumentFromFragmentDefinition(FragmentDefinition fragmentDefinition) { 54 | return Document.newDocument().definition(fragmentDefinition).build(); 55 | } 56 | 57 | private boolean documentsEqual(Document doc1, Document doc2) { 58 | return doc1.toString().length() == doc2.toString().length(); 59 | } 60 | 61 | public FragmentDefinition getGqlFragmentDefinition(String fragmentName) { 62 | return gqlFragmentDefinitions.get(fragmentName); 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/test/java/com/pdstat/gqlextractor/repo/GqlFragmentDefinitionsRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.pdstat.gqlextractor.repo; 2 | 3 | import static org.mockito.ArgumentMatchers.any; 4 | 5 | import com.pdstat.gqlextractor.service.GqlMergerService; 6 | import graphql.language.Document; 7 | import graphql.language.FragmentDefinition; 8 | import graphql.parser.Parser; 9 | import org.junit.jupiter.api.Assertions; 10 | import org.junit.jupiter.api.Test; 11 | import org.junit.jupiter.api.extension.ExtendWith; 12 | import org.mockito.InjectMocks; 13 | import org.mockito.Mock; 14 | import org.mockito.Mockito; 15 | import org.mockito.junit.jupiter.MockitoExtension; 16 | 17 | import java.util.List; 18 | 19 | @ExtendWith(MockitoExtension.class) 20 | public class GqlFragmentDefinitionsRepositoryTest { 21 | 22 | @Mock 23 | private GqlDocumentRepository gqlDocumentRepository; 24 | 25 | @Mock 26 | private GqlMergerService gqlMergerService; 27 | 28 | @InjectMocks 29 | private GqlFragmentDefinitionsRepository gqlFragmentDefinitionsRepository; 30 | 31 | @Test 32 | void testInitFragmentDefinitionsNewFragment() { 33 | String noticeFieldsFragment = "fragment NoticeFields on Notice { localizedTitle localizedDescription url }"; 34 | Document noticeFieldsFragmentDocument = new Parser().parseDocument(noticeFieldsFragment); 35 | 36 | Mockito.when(gqlDocumentRepository.getGqlDocuments()).thenReturn(List.of(noticeFieldsFragmentDocument)); 37 | 38 | gqlFragmentDefinitionsRepository.initFragmentDefinitions(); 39 | 40 | FragmentDefinition noticeFieldsFragmentDefinition = gqlFragmentDefinitionsRepository.getGqlFragmentDefinition("NoticeFields"); 41 | Assertions.assertNotNull(noticeFieldsFragmentDefinition); 42 | } 43 | 44 | @Test 45 | void testInitFragmentDefinitionsExistingFragment() { 46 | String noticeFieldsFragment1 = "fragment NoticeFields on Notice { localizedTitle localizedDescription url }"; 47 | Document noticeFieldsFragmentDocument1 = new Parser().parseDocument(noticeFieldsFragment1); 48 | String noticeFieldsFragment2 = "fragment NoticeFields on Notice { localizedTitle localizedDescription url anotherUrl }"; 49 | Document noticeFieldsFragmentDocument2 = new Parser().parseDocument(noticeFieldsFragment2); 50 | 51 | Mockito.when(gqlDocumentRepository.getGqlDocuments()).thenReturn(List.of(noticeFieldsFragmentDocument1, noticeFieldsFragmentDocument2)); 52 | Mockito.when(gqlMergerService.mergeGraphQLDocuments(any(), any())).thenReturn(noticeFieldsFragmentDocument2); 53 | 54 | gqlFragmentDefinitionsRepository.initFragmentDefinitions(); 55 | 56 | FragmentDefinition noticeFieldsFragmentDefinition = gqlFragmentDefinitionsRepository.getGqlFragmentDefinition("NoticeFields"); 57 | Assertions.assertNotNull(noticeFieldsFragmentDefinition); 58 | FragmentDefinition expectedFragmentDefinition = (FragmentDefinition) noticeFieldsFragmentDocument2.getDefinitions().get(0); 59 | Assertions.assertEquals(expectedFragmentDefinition.getName(), ((FragmentDefinition) noticeFieldsFragmentDefinition).getName()); 60 | Assertions.assertEquals(expectedFragmentDefinition.getSelectionSet(), ((FragmentDefinition) noticeFieldsFragmentDefinition).getSelectionSet()); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/test/java/com/pdstat/gqlextractor/service/GqlMergerServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.pdstat.gqlextractor.service; 2 | 3 | 4 | import graphql.language.AstPrinter; 5 | import graphql.language.Document; 6 | import graphql.parser.Parser; 7 | import org.junit.jupiter.api.Assertions; 8 | import org.junit.jupiter.api.Test; 9 | 10 | public class GqlMergerServiceTest { 11 | 12 | // @Test 13 | // void testMergeGraphQLDocuments() { 14 | // String fullQuery = "query GetSupply($input: SupplyInput!) { supply(input: $input) { stations { stationId stationName" + 15 | // " location { lat lng } bikesAvailable bikeDocksAvailable ebikesAvailable scootersAvailable totalBikesAvailable" + 16 | // " totalRideablesAvailable isValet isOffline isLightweight notices { ...NoticeFields } siteId ebikes { rideableName" + 17 | // " batteryStatus { distanceRemaining { value unit } percent } } scooters { rideableName batteryStatus { " + 18 | // "distanceRemaining { value unit } percent } } lastUpdatedMs } rideables { rideableId location { lat lng } " + 19 | // "rideableType photoUrl batteryStatus { distanceRemaining { value unit } percent } } notices { ...NoticeFields " + 20 | // "} requestErrors { ...NoticeFields } } }"; 21 | // Document fullDocument = new Parser().parseDocument(fullQuery); 22 | // 23 | // String query1 = "query GetSupply($input: SupplyInput) { supply(input: $input) { stations { stationId stationName" + 24 | // " location { lat lng } bikesAvailable ebikesAvailable scootersAvailable totalBikesAvailable" + 25 | // " totalRideablesAvailable isValet isOffline isLightweight notices { ...NoticeFields } siteId ebikes { rideableName" + 26 | // " batteryStatus { distanceRemaining { value unit } percent } } scooters { rideableName batteryStatus { " + 27 | // "distanceRemaining { value unit } percent } } lastUpdatedMs } rideables { rideableId location { lat lng } " + 28 | // "rideableType photoUrl batteryStatus { distanceRemaining { value unit } percent } } notices { ...NoticeFields " + 29 | // "} requestErrors { ...NoticeFields } } }"; 30 | // Document document1 = new Parser().parseDocument(query1); 31 | // 32 | // String query2 = "query GetSupply($input: SupplyInput) { supply(input: $input) { stations { stationId stationName" + 33 | // " location { lat lng } bikeDocksAvailable ebikesAvailable scootersAvailable " + 34 | // " totalRideablesAvailable isValet isOffline isLightweight notices { ...NoticeFields } siteId ebikes { rideableName" + 35 | // " batteryStatus { percent } } scooters { rideableName batteryStatus { " + 36 | // "distanceRemaining { value unit } percent } } lastUpdatedMs } rideables { rideableId location { lng } " + 37 | // "rideableType photoUrl batteryStatus { distanceRemaining { value unit } percent } } notices { ...NoticeFields " + 38 | // "} } }"; 39 | // Document document2 = new Parser().parseDocument(query2); 40 | // 41 | // GqlMergerService gqlMergerService = new GqlMergerService(); 42 | // Document mergedDocument = gqlMergerService.mergeGraphQLDocuments(document1, document2); 43 | // 44 | // Assertions.assertEquals(AstPrinter.printAst(fullDocument).length(), AstPrinter.printAst(mergedDocument).length()); 45 | // 46 | // } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/pdstat/gqlextractor/service/GqlPathFinder.java: -------------------------------------------------------------------------------- 1 | package com.pdstat.gqlextractor.service; 2 | 3 | import java.util.*; 4 | import graphql.language.*; 5 | import graphql.parser.*; 6 | import org.springframework.stereotype.Component; 7 | 8 | @Component 9 | public class GqlPathFinder { 10 | 11 | 12 | private Map graphqlToMap(String graphqlString) { 13 | Document document = new Parser().parseDocument(graphqlString); 14 | return graphqlToMap(document); 15 | } 16 | 17 | private Map graphqlToMap(Document document) { 18 | Map result = new LinkedHashMap<>(); 19 | for (Definition definition : document.getDefinitions()) { 20 | if (definition instanceof FragmentDefinition fragmentDefinition) { 21 | result.put(fragmentDefinition.getName(), nodeToMap(fragmentDefinition.getSelectionSet())); 22 | } else if (definition instanceof OperationDefinition operationDefinition) { 23 | result.put(operationDefinition.getName(), nodeToMap(operationDefinition.getSelectionSet())); 24 | } 25 | } 26 | 27 | return result; 28 | } 29 | 30 | private Map nodeToMap(SelectionSet selectionSet) { 31 | Map map = new LinkedHashMap<>(); 32 | if (selectionSet != null) { 33 | for (Selection selection : selectionSet.getSelections()) { 34 | if (selection instanceof Field field) { 35 | map.put(field.getName(), nodeToMap(field.getSelectionSet())); 36 | } else if (selection instanceof FragmentSpread fragmentSpread) { 37 | map.put(fragmentSpread.getName(), null); 38 | } else if (selection instanceof InlineFragment inlineFragment) { 39 | map.put(inlineFragment.getTypeCondition().getName(), nodeToMap(inlineFragment.getSelectionSet())); 40 | } 41 | } 42 | } 43 | return map; 44 | } 45 | 46 | // Recursively find paths to the target field. 47 | private List findFieldPaths(Map definition, String targetField) { 48 | return findFieldPaths(definition, targetField, new ArrayList<>(), new ArrayList<>()); 49 | } 50 | 51 | private List findFieldPaths(Map definition, String targetField, List currentPath, List paths) { 52 | for (Map.Entry entry : definition.entrySet()) { 53 | List newPath = new ArrayList<>(currentPath); 54 | newPath.add(entry.getKey()); 55 | 56 | if (entry.getKey().equals(targetField)) { 57 | Collections.reverse(newPath); 58 | paths.add(String.join(" -> ", newPath)); 59 | } 60 | 61 | if (entry.getValue() instanceof Map) { 62 | findFieldPaths((Map) entry.getValue(), targetField, newPath, paths); 63 | } 64 | } 65 | return paths; 66 | } 67 | 68 | public List findFieldPaths(Document graphqlDocument, String targetField) { 69 | try { 70 | Map gqlDocumentMap = graphqlToMap(graphqlDocument); 71 | return findFieldPaths(gqlDocumentMap, targetField); 72 | } catch (InvalidSyntaxException ignored) { 73 | return new ArrayList<>(); 74 | } 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/com/pdstat/gqlextractor/service/GqlRequestFactoryService.java: -------------------------------------------------------------------------------- 1 | package com.pdstat.gqlextractor.service; 2 | 3 | import com.pdstat.gqlextractor.Constants; 4 | import com.pdstat.gqlextractor.model.GqlRequest; 5 | import com.pdstat.gqlextractor.repo.DefaultParamsRepository; 6 | import graphql.language.*; 7 | import org.springframework.stereotype.Service; 8 | 9 | import java.util.HashMap; 10 | import java.util.List; 11 | 12 | @Service 13 | public class GqlRequestFactoryService { 14 | 15 | private final DefaultParamsRepository defaultParamsRepository; 16 | 17 | public GqlRequestFactoryService(DefaultParamsRepository defaultParamsRepository) { 18 | this.defaultParamsRepository = defaultParamsRepository; 19 | } 20 | 21 | public GqlRequest createGqlRequest(Document document) { 22 | GqlRequest gqlRequest = new GqlRequest(); 23 | gqlRequest.setQuery(AstPrinter.printAstCompact(document)); 24 | setVariables(document, gqlRequest); 25 | return gqlRequest; 26 | } 27 | 28 | private void setVariables(Document document, GqlRequest gqlRequest) { 29 | document.getDefinitions().forEach(definition -> { 30 | if (definition instanceof graphql.language.OperationDefinition) { 31 | gqlRequest.setOperationName(((graphql.language.OperationDefinition) definition).getName()); 32 | ((graphql.language.OperationDefinition) definition).getVariableDefinitions().forEach(variableDefinition -> { 33 | String argName = variableDefinition.getName(); 34 | Type argType = variableDefinition.getType(); 35 | Object defaultValue = defaultParamsRepository.getDefaultParam(argName); 36 | if (defaultValue == null) { 37 | defaultValue = getDefaultValue(argType); 38 | } 39 | gqlRequest.setVariable(argName, defaultValue); 40 | }); 41 | } 42 | }); 43 | 44 | } 45 | 46 | private Object getDefaultValue(Type type) { 47 | boolean isList = false; 48 | String typeName = null; 49 | 50 | // Unwrap NonNullType if present 51 | while (type instanceof NonNullType) { 52 | type = ((NonNullType) type).getType(); 53 | } 54 | 55 | // Check if it's a ListType and unwrap it 56 | if (type instanceof ListType) { 57 | isList = true; 58 | type = ((ListType) type).getType(); 59 | 60 | // Unwrap NonNullType inside the list if present 61 | while (type instanceof NonNullType) { 62 | type = ((NonNullType) type).getType(); 63 | } 64 | } 65 | 66 | if (type instanceof TypeName) { 67 | typeName = ((TypeName) type).getName(); 68 | return switch (typeName) { 69 | case Constants.Gql.Scalar.INT -> isList ? List.of(0) : 0; 70 | case Constants.Gql.Scalar.FLOAT -> isList ? List.of(0.0F) : 0.0F; 71 | case Constants.Gql.Scalar.LONG -> isList ? List.of(0L) : 0L; 72 | case Constants.Gql.Scalar.BOOLEAN -> isList ? List.of(false) : false; 73 | case Constants.Gql.Scalar.STRING, Constants.Gql.Scalar.ID -> isList ? List.of("") : ""; 74 | default -> isList ? List.of(new HashMap()) : new HashMap(); 75 | }; 76 | } 77 | 78 | throw new IllegalArgumentException("Unexpected GraphQL Type structure"); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/test/java/com/pdstat/gqlextractor/repo/GqlFieldRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.pdstat.gqlextractor.repo; 2 | 3 | import graphql.language.Document; 4 | import graphql.parser.Parser; 5 | import org.junit.jupiter.api.Assertions; 6 | import org.junit.jupiter.api.Test; 7 | import org.junit.jupiter.api.extension.ExtendWith; 8 | import org.mockito.InjectMocks; 9 | import org.mockito.Mock; 10 | import org.mockito.Mockito; 11 | import org.mockito.junit.jupiter.MockitoExtension; 12 | 13 | import java.util.Arrays; 14 | import java.util.List; 15 | 16 | @ExtendWith(MockitoExtension.class) 17 | public class GqlFieldRepositoryTest { 18 | 19 | @Mock 20 | private GqlDocumentRepository gqlDocumentRepository; 21 | 22 | @InjectMocks 23 | private GqlFieldRepository gqlFieldRepository; 24 | 25 | @Test 26 | void testInitGqlFields() { 27 | String getSupplyQuery = "query GetSupply($input: SupplyInput) { supply(input: $input) { stations { stationId " + 28 | "stationName location { lat lng } bikesAvailable bikeDocksAvailable ebikesAvailable scootersAvailable " + 29 | "totalBikesAvailable totalRideablesAvailable isValet isOffline isLightweight notices { ...NoticeFields }" + 30 | "siteId ebikes { rideableName batteryStatus { distanceRemaining { value unit } percent } } scooters { " + 31 | "rideableName batteryStatus { distanceRemaining { value unit } percent } } lastUpdatedMs } rideables { " + 32 | "rideableId location { lat lng } rideableType photoUrl batteryStatus { distanceRemaining { value unit } " + 33 | "percent } } notices { ...NoticeFields } requestErrors { ...NoticeFields } } }"; 34 | String initMapQuery = "query InitMap { config { map { mapDataRegionCodes } } serviceAreas { polygons { holes polyline " + 35 | "} overrides { color type polygons { holes polyline } } type } currentMarket { singleRide { unlockPriceLabel " + 36 | "minutePriceLabel } } }"; 37 | Document getSupplyDocument = new Parser().parseDocument(getSupplyQuery); 38 | Document initMapDocument = new Parser().parseDocument(initMapQuery); 39 | 40 | 41 | Mockito.when(gqlDocumentRepository.getGqlDocuments()).thenReturn(List.of(getSupplyDocument, initMapDocument)); 42 | 43 | gqlFieldRepository.initGqlFields(); 44 | 45 | List expectedFields = getExpectedFields(); 46 | 47 | List gqlFields = gqlFieldRepository.getGqlFields(); 48 | Assertions.assertFalse(gqlFields.isEmpty()); 49 | Assertions.assertEquals(expectedFields.size(), gqlFields.size()); 50 | Assertions.assertTrue(gqlFields.containsAll(expectedFields)); 51 | Assertions.assertEquals(expectedFields, gqlFields); 52 | } 53 | 54 | private static List getExpectedFields() { 55 | String fieldsArray = "batteryStatus,bikeDocksAvailable,bikesAvailable,color,config,currentMarket," + 56 | "distanceRemaining,ebikes,ebikesAvailable,holes,isLightweight,isOffline,isValet,lastUpdatedMs,lat,lng," + 57 | "location,map,mapDataRegionCodes,minutePriceLabel,notices,overrides,percent,photoUrl,polygons,polyline," + 58 | "requestErrors,rideableId,rideableName,rideableType,rideables,scooters,scootersAvailable,serviceAreas," + 59 | "singleRide,siteId,stationId,stationName,stations,supply,totalBikesAvailable,totalRideablesAvailable," + 60 | "type,unit,unlockPriceLabel,value"; 61 | String[] fields = fieldsArray.split(","); 62 | return List.of(fields); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/pdstat/gqlextractor/service/GqlSchemaPathFinder.java: -------------------------------------------------------------------------------- 1 | package com.pdstat.gqlextractor.service; 2 | 3 | import graphql.language.*; 4 | import org.springframework.stereotype.Component; 5 | 6 | import java.util.*; 7 | 8 | @Component 9 | public class GqlSchemaPathFinder { 10 | 11 | private final Map> fieldTypes = new HashMap<>(); 12 | private final Set visitedPaths = new HashSet<>(); 13 | 14 | private void buildGraph(Document document) { 15 | for (Definition definition : document.getDefinitions()) { 16 | if (definition instanceof ObjectTypeDefinition typeDef) { 17 | fieldTypes.putIfAbsent(typeDef.getName(), new HashMap<>()); 18 | 19 | for (FieldDefinition field : typeDef.getFieldDefinitions()) { 20 | fieldTypes.get(typeDef.getName()).put(field.getName(), extractTypeName(field.getType())); 21 | } 22 | } 23 | } 24 | } 25 | 26 | private String extractTypeName(Type type) { 27 | if (type instanceof TypeName) { 28 | return ((TypeName) type).getName(); 29 | } else if (type instanceof NonNullType) { 30 | return extractTypeName(((NonNullType) type).getType()); 31 | } else if (type instanceof ListType) { 32 | return extractTypeName(((ListType) type).getType()); 33 | } 34 | return null; 35 | } 36 | 37 | /** 38 | * Find all paths to a target field in the GraphQL schema 39 | * @param document GraphQL schema document 40 | * @param targetField Field name to search for 41 | * @param maxDepth Maximum depth to search 42 | * @return List of paths to the target field 43 | */ 44 | public List findFieldPaths(Document document, String targetField, int maxDepth) { 45 | buildGraph(document); 46 | List paths = new ArrayList<>(); 47 | Queue> queue = new LinkedList<>(); 48 | 49 | List rootTypes = Arrays.asList("Query", "Mutation", "Subscription"); 50 | for (String rootType : rootTypes) { 51 | if (fieldTypes.containsKey(rootType)) { 52 | queue.add(Collections.singletonList(rootType)); 53 | } 54 | } 55 | 56 | while (!queue.isEmpty()) { 57 | List path = queue.poll(); 58 | if (path.size() > maxDepth) continue; 59 | 60 | String currentType = path.get(path.size() - 1); 61 | if (!fieldTypes.containsKey(currentType)) continue; 62 | 63 | for (Map.Entry entry : fieldTypes.get(currentType).entrySet()) { 64 | String fieldName = entry.getKey(); 65 | String nextType = entry.getValue(); 66 | 67 | List newPath = new ArrayList<>(path); 68 | newPath.add(fieldName); 69 | 70 | // Generate a unique key for path tracking to avoid cycles 71 | String pathKey = String.join(" -> ", newPath); 72 | if (visitedPaths.contains(pathKey)) continue; 73 | visitedPaths.add(pathKey); 74 | 75 | if (fieldName.equals(targetField)) { 76 | Collections.reverse(newPath); 77 | paths.add(String.join(" -> ", newPath)); 78 | } else if (fieldTypes.containsKey(nextType)) { 79 | List nextPath = new ArrayList<>(newPath); 80 | nextPath.add(nextType); 81 | queue.add(nextPath); 82 | } 83 | } 84 | } 85 | return paths; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/com/pdstat/gqlextractor/GqlExtractor.java: -------------------------------------------------------------------------------- 1 | package com.pdstat.gqlextractor; 2 | 3 | import com.pdstat.gqlextractor.service.GqlExtractorOutputHandlerService; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.ApplicationArguments; 8 | import org.springframework.boot.CommandLineRunner; 9 | import org.springframework.boot.SpringApplication; 10 | import org.springframework.boot.autoconfigure.SpringBootApplication; 11 | 12 | @SpringBootApplication 13 | public class GqlExtractor implements CommandLineRunner { 14 | 15 | private static final Logger logger = LoggerFactory.getLogger(GqlExtractor.class); 16 | 17 | @Autowired 18 | private ApplicationArguments appArgs; 19 | @Autowired 20 | private GqlExtractorOutputHandlerService gqlExtractorOutputHandlerService; 21 | 22 | public static void main(String[] args) { 23 | SpringApplication.run(GqlExtractor.class, args); 24 | } 25 | 26 | @Override 27 | public void run(String... args) throws Exception { 28 | if (appArgs.containsOption(Constants.Arguments.HELP)) { 29 | logger.info("GqlExtractor is an application that extracts .graphql and .json files from a directory of javascript files that contain GraphQL strings."); 30 | logger.info("The application requires the following arguments:"); 31 | logger.info(" --input-directory= : The directory containing the javascript files with embedded GQL strings."); 32 | logger.info(" --input-urls= : The path to a wordlist of urls to scan."); 33 | logger.info(" --input-schema= : URL to a graphQL endpoint with introspection enabled or the path to a file containing the json response of an introspection query."); 34 | logger.info(" --input-operations= : The directory containing previously extracted .graphql operations, this avoids resource intensive Javascript AST parsing."); 35 | logger.info(" --request-header= : Request header key/value to set in introspection requests e.g. --request-header=\"Api-Key1: XXXX\" --request-header=\"Api-Key2: YYYY\"."); 36 | logger.info(" --search-field= : The field name paths to search for in the schema/operations."); 37 | logger.info(" --depth= : Depth of the field path search, defaults to 10 if not specified."); 38 | logger.info(" --default-params= : The path to a json file of default parameter values."); 39 | logger.info(" --output-directory= : The directory where the generated files will be saved."); 40 | logger.info(" --output-mode= : The output mode for the generated files. Possible values are 'requests', 'operations', 'fields', 'paths' and 'all'. The default value is 'requests'."); 41 | System.exit(0); 42 | } 43 | 44 | if (!appArgs.containsOption(Constants.Arguments.INPUT_DIRECTORY) && !appArgs.containsOption(Constants.Arguments.INPUT_URLS) 45 | && !appArgs.containsOption(Constants.Arguments.INPUT_SCHEMA) && !appArgs.containsOption(Constants.Arguments.INPUT_OPERATIONS)) { 46 | logger.error("--input-directory or --input-urls or --input-schema or --input-operations not provided. Exiting application. Use the --help option for more information."); 47 | System.exit(1); 48 | } 49 | 50 | if (!appArgs.containsOption(Constants.Arguments.OUTPUT_DIRECTORY)) { 51 | logger.error("--output-directory not provided. Exiting application. Use the --help option for more information."); 52 | System.exit(1); 53 | } 54 | 55 | gqlExtractorOutputHandlerService.handleGQLExtractorOutput(); 56 | System.exit(0); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/pdstat/gqlextractor/graal/GraalAcornWalker.java: -------------------------------------------------------------------------------- 1 | package com.pdstat.gqlextractor.graal; 2 | 3 | import com.pdstat.gqlextractor.service.ResourceService; 4 | import jakarta.annotation.PostConstruct; 5 | import jakarta.annotation.PreDestroy; 6 | import org.graalvm.polyglot.Context; 7 | import org.graalvm.polyglot.Engine; 8 | import org.graalvm.polyglot.PolyglotException; 9 | import org.graalvm.polyglot.Value; 10 | 11 | import org.slf4j.LoggerFactory; 12 | import org.slf4j.Logger; 13 | import org.springframework.core.io.Resource; 14 | import org.springframework.stereotype.Component; 15 | 16 | import java.io.IOException; 17 | import java.util.concurrent.ConcurrentHashMap; 18 | 19 | @Component 20 | public class GraalAcornWalker { 21 | 22 | private static final Logger logger = LoggerFactory.getLogger(GraalAcornWalker.class); 23 | 24 | private final ResourceService resourceService; 25 | private final Resource acornResource; 26 | private final Resource acornWalkResource; 27 | private final Resource parseDocsResource; 28 | 29 | private final Engine engine; 30 | private final ConcurrentHashMap contextMap = new ConcurrentHashMap<>(); 31 | private String acornScript; 32 | private String acornWalkScript; 33 | private String parseScript; 34 | 35 | public GraalAcornWalker(ResourceService resourceService, 36 | @org.springframework.beans.factory.annotation.Value("classpath:acorn.js")Resource acornResource, 37 | @org.springframework.beans.factory.annotation.Value("classpath:walk.js")Resource acornWalkResource, 38 | @org.springframework.beans.factory.annotation.Value("classpath:parseDocs.js")Resource parseDocsResource) { 39 | this.resourceService = resourceService; 40 | this.acornResource = acornResource; 41 | this.acornWalkResource = acornWalkResource; 42 | this.parseDocsResource = parseDocsResource; 43 | this.engine = Engine.newBuilder().build(); 44 | } 45 | 46 | @PostConstruct 47 | void initialise() { 48 | try { 49 | // Read script files once and store them as Strings 50 | this.acornScript = resourceService.readResourceFileContent(acornResource); 51 | this.acornWalkScript = resourceService.readResourceFileContent(acornWalkResource); 52 | this.parseScript = resourceService.readResourceFileContent(parseDocsResource); 53 | } catch (IOException e) { 54 | logger.error("Error loading JavaScript libraries", e); 55 | throw new RuntimeException("Failed to initialize GraalAcornWalker", e); 56 | } 57 | } 58 | 59 | @PreDestroy 60 | void cleanup() { 61 | contextMap.values().forEach(Context::close); 62 | engine.close(); 63 | } 64 | 65 | public String extractMatchedObjects(String javascript, String visitorScript) { 66 | try { 67 | Context context = getContextForThread(); 68 | 69 | context.eval("js", "var matchedObjects = [];"); 70 | 71 | context.eval("js", parseScript); 72 | 73 | Value parseFunction = context.eval("js", "acorn.parse"); 74 | Value parseJsFunction = context.eval("js", "parseJavascript"); 75 | 76 | Value parseAst = parseFunction.execute(javascript, 77 | context.eval("js", "({ ecmaVersion: 'latest', sourceType: 'module' })")); 78 | 79 | Value visitorObject = context.eval("js", visitorScript); 80 | return parseJsFunction.execute(parseAst, visitorObject).asString(); 81 | } catch (PolyglotException e) { 82 | logger.error("Error during JavaScript execution", e); 83 | return null; 84 | } 85 | } 86 | 87 | private Context getContextForThread() { 88 | return contextMap.computeIfAbsent(Thread.currentThread().getId(), threadId -> { 89 | Context ctx = Context.newBuilder("js") 90 | .engine(engine) 91 | .option("js.shared-array-buffer", "true") // Enables multi-threading support 92 | .allowAllAccess(true) 93 | .build(); 94 | 95 | // Load JavaScript libraries into the context 96 | ctx.eval("js", acornScript); 97 | ctx.eval("js", acornWalkScript); 98 | ctx.eval("js", parseScript); 99 | 100 | return ctx; 101 | }); 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /src/main/java/com/pdstat/gqlextractor/repo/GqlOperationsRepository.java: -------------------------------------------------------------------------------- 1 | package com.pdstat.gqlextractor.repo; 2 | 3 | import com.pdstat.gqlextractor.service.GqlMergerService; 4 | import graphql.language.Definition; 5 | import graphql.language.Document; 6 | import graphql.language.FragmentDefinition; 7 | import graphql.language.OperationDefinition; 8 | import jakarta.annotation.PostConstruct; 9 | import org.springframework.stereotype.Repository; 10 | 11 | import java.util.HashMap; 12 | import java.util.HashSet; 13 | import java.util.Map; 14 | import java.util.Set; 15 | import java.util.regex.Matcher; 16 | import java.util.regex.Pattern; 17 | 18 | @Repository 19 | public class GqlOperationsRepository { 20 | 21 | private static final Pattern FRAGMENT_SPREAD_PATTERN = Pattern.compile("FragmentSpread\\{name='([^']+)'"); 22 | 23 | private final GqlDocumentRepository gqlDocumentRepository; 24 | private final GqlFragmentDefinitionsRepository gqlFragmentDefinitionsRepository; 25 | private final GqlMergerService gqlMergerService; 26 | 27 | private final Map gqlOperations = new HashMap<>(); 28 | 29 | public GqlOperationsRepository(GqlDocumentRepository gqlDocumentRepository, 30 | GqlFragmentDefinitionsRepository gqlFragmentDefinitionsRepository, 31 | GqlMergerService gqlMergerService) { 32 | this.gqlDocumentRepository = gqlDocumentRepository; 33 | this.gqlFragmentDefinitionsRepository = gqlFragmentDefinitionsRepository; 34 | this.gqlMergerService = gqlMergerService; 35 | } 36 | 37 | @PostConstruct 38 | void initGqlOperations() { 39 | for (Document document : gqlDocumentRepository.getGqlDocuments()) { 40 | for (Definition definition : document.getDefinitions()) { 41 | if (definition instanceof OperationDefinition operationDefinition) { 42 | String operationName = operationDefinition.getName(); 43 | if (gqlOperations.containsKey(operationName)) { 44 | Document storedOperation = gqlOperations.get(operationName); 45 | 46 | if (!documentsEqual(storedOperation, document)) { 47 | document = gqlMergerService.mergeGraphQLDocuments(storedOperation, document); 48 | } 49 | } 50 | gqlOperations.put(operationName, addMissingFragmentDefinitions(document)); 51 | } 52 | } 53 | } 54 | } 55 | 56 | private boolean documentsEqual(Document doc1, Document doc2) { 57 | return doc1.toString().length() == doc2.toString().length(); 58 | } 59 | 60 | private Document addMissingFragmentDefinitions(Document document) { 61 | Set addedFragments = new HashSet<>(); 62 | Document.Builder documentBuilder = Document.newDocument(); 63 | 64 | // Copy existing definitions 65 | for (Definition definition : document.getDefinitions()) { 66 | documentBuilder.definition(definition); 67 | if (definition instanceof FragmentDefinition) { 68 | addedFragments.add(((FragmentDefinition) definition).getName()); 69 | } 70 | } 71 | 72 | // Recursively resolve missing fragment definitions 73 | resolveMissingFragments(documentBuilder, addedFragments, getFragmentSpreads(document.toString())); 74 | 75 | return documentBuilder.build(); 76 | } 77 | 78 | private void resolveMissingFragments(Document.Builder documentBuilder, Set addedFragments, Set fragmentSpreads) { 79 | for (String fragmentSpread : fragmentSpreads) { 80 | if (!addedFragments.contains(fragmentSpread)) { 81 | FragmentDefinition fragmentDefinition = gqlFragmentDefinitionsRepository.getGqlFragmentDefinition(fragmentSpread); 82 | if (fragmentDefinition != null) { 83 | documentBuilder.definition(fragmentDefinition); 84 | addedFragments.add(fragmentSpread); 85 | 86 | // Recursively resolve dependencies of the added fragment 87 | resolveMissingFragments(documentBuilder, addedFragments, getFragmentSpreads(fragmentDefinition.toString())); 88 | } 89 | } 90 | } 91 | } 92 | 93 | private Set getFragmentSpreads(String nodeString) { 94 | Set fragmentSpreads = new HashSet<>(); 95 | Matcher matcher = FRAGMENT_SPREAD_PATTERN.matcher(nodeString); 96 | while (matcher.find()) { 97 | fragmentSpreads.add(matcher.group(1)); 98 | } 99 | return fragmentSpreads; 100 | } 101 | 102 | public Document getGqlOperation(String operationName) { 103 | return gqlOperations.get(operationName); 104 | } 105 | 106 | public Map getGqlOperations() { 107 | return gqlOperations; 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /src/main/java/com/pdstat/gqlextractor/extractor/GqlDocumentExtractor.java: -------------------------------------------------------------------------------- 1 | package com.pdstat.gqlextractor.extractor; 2 | 3 | import com.fasterxml.jackson.core.type.TypeReference; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.pdstat.gqlextractor.graal.GraalAcornWalker; 6 | import com.pdstat.gqlextractor.service.ResourceService; 7 | import graphql.language.Document; 8 | import graphql.parser.InvalidSyntaxException; 9 | import graphql.parser.Parser; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | import org.springframework.beans.factory.annotation.Value; 13 | import org.springframework.core.io.Resource; 14 | import org.springframework.stereotype.Component; 15 | 16 | import java.io.IOException; 17 | import java.util.ArrayList; 18 | import java.util.HashSet; 19 | import java.util.List; 20 | import java.util.Set; 21 | import java.util.stream.Collectors; 22 | 23 | @Component 24 | public class GqlDocumentExtractor { 25 | 26 | private static final Logger logger = LoggerFactory.getLogger(GqlDocumentExtractor.class); 27 | private static final String JSON_STRING_ARRAY_PREFIX = "[\""; 28 | 29 | private final Resource gqlDocsVisitorResource; 30 | private final Resource gqlStringsVisitorResource; 31 | private final GraalAcornWalker walker; 32 | private final ResourceService resourceService; 33 | private final ObjectMapper mapper; 34 | 35 | public GqlDocumentExtractor(@Value("classpath:gql-docs-visitor.js") Resource gqlDocsVisitorResource, 36 | @Value("classpath:gql-strings-visitor.js") Resource gqlStringsVisitorResource, 37 | GraalAcornWalker walker, ResourceService resourceService, ObjectMapper mapper) { 38 | this.gqlDocsVisitorResource = gqlDocsVisitorResource; 39 | this.gqlStringsVisitorResource = gqlStringsVisitorResource; 40 | this.walker = walker; 41 | this.resourceService = resourceService; 42 | this.mapper = mapper; 43 | } 44 | 45 | public List extract(String javascript) { 46 | List documents = new ArrayList<>(); 47 | try { 48 | documents.addAll(extractDocumentsWithVisitor(javascript, documents, 49 | resourceService.readResourceFileContent(gqlDocsVisitorResource))); 50 | documents.addAll(extractDocumentsWithVisitor(javascript, documents, 51 | resourceService.readResourceFileContent(gqlStringsVisitorResource))); 52 | } catch (IOException e) { 53 | logger.error("Error extracting documents from javascript", e); 54 | } 55 | 56 | return documents; 57 | } 58 | 59 | private List extractDocumentsWithVisitor(String javascript, List documents, 60 | String visitorScript) throws IOException { 61 | String matchedDocumentStrings = walker.extractMatchedObjects(javascript, visitorScript); 62 | return parseDocuments(matchedDocumentStrings); 63 | } 64 | 65 | private List parseDocuments(String matchedDocumentStrings) throws IOException { 66 | List docs = new ArrayList<>(); 67 | if (jsonStringArray(matchedDocumentStrings)) { 68 | List documentStrings = mapper.readValue(matchedDocumentStrings, new TypeReference>() {}); 69 | if (gqlLanguageString(matchedDocumentStrings)) { 70 | extractGqlLanguageStrings(documentStrings, docs); 71 | } else { 72 | for (String doc : documentStrings) { 73 | docs.add(mapper.readValue(doc, Document.class)); 74 | } 75 | } 76 | } else { 77 | docs = mapper.readValue(matchedDocumentStrings, new TypeReference>() {}); 78 | } 79 | return docs; 80 | } 81 | 82 | private void extractGqlLanguageStrings(List documentStrings, List docs) { 83 | Parser gqlParser = new Parser(); 84 | for (String doc : documentStrings) { 85 | try { 86 | int startIndex = getStartIndex(doc); 87 | doc = doc.substring(startIndex); 88 | docs.add(gqlParser.parseDocument(doc)); 89 | } catch (InvalidSyntaxException e) { 90 | logger.error("Error parsing document: {}", doc, e); 91 | } 92 | } 93 | } 94 | 95 | private int getStartIndex(String doc) { 96 | int queryIndex = doc.indexOf("query "); 97 | int mutationIndex = doc.indexOf("mutation "); 98 | int subscriptionIndex = doc.indexOf("subscription "); 99 | int fragmentIndex = doc.indexOf("fragment "); 100 | List indexList = List.of(queryIndex, mutationIndex, subscriptionIndex, fragmentIndex); 101 | return indexList.stream().filter(index -> index >= 0).sorted().toList().get(0); 102 | } 103 | 104 | private boolean gqlLanguageString(String matchedDocumentStrings) { 105 | return matchedDocumentStrings.contains("query ") || matchedDocumentStrings.contains("mutation ") || 106 | matchedDocumentStrings.contains("subscription ") || matchedDocumentStrings.contains("fragment "); 107 | } 108 | 109 | private boolean jsonStringArray(String matchedDocumentStrings) { 110 | return matchedDocumentStrings.startsWith(JSON_STRING_ARRAY_PREFIX); 111 | } 112 | 113 | 114 | } 115 | -------------------------------------------------------------------------------- /src/main/java/com/pdstat/gqlextractor/service/GqlMergerService.java: -------------------------------------------------------------------------------- 1 | package com.pdstat.gqlextractor.service; 2 | 3 | import graphql.language.*; 4 | import org.springframework.stereotype.Service; 5 | 6 | import java.util.*; 7 | 8 | @Service 9 | public class GqlMergerService { 10 | 11 | /** 12 | * Merge two GraphQL documents into a single document. 13 | * @param doc1 The first document 14 | * @param doc2 The second document 15 | * @return The merged document 16 | */ 17 | public Document mergeGraphQLDocuments(Document doc1, Document doc2) { 18 | List mergedDefinitions = new ArrayList<>(); 19 | 20 | // Merge operations dynamically 21 | Map mergedOperations = new LinkedHashMap<>(); 22 | Map mergedFragments = new LinkedHashMap<>(); 23 | 24 | for (Document doc : Arrays.asList(doc1, doc2)) { 25 | for (Definition definition : doc.getDefinitions()) { 26 | if (definition instanceof OperationDefinition opDef) { 27 | String operationName = opDef.getName() != null ? opDef.getName() : "Anonymous"; 28 | mergedOperations.merge(operationName, opDef, this::mergeOperationDefinitions); 29 | } else if (definition instanceof FragmentDefinition fragDef) { 30 | mergedFragments.merge(fragDef.getName(), fragDef, this::mergeFragmentDefinitions); 31 | } else { 32 | mergedDefinitions.add(definition); // Keep any other definitions 33 | } 34 | } 35 | } 36 | 37 | mergedDefinitions.addAll(mergedOperations.values()); 38 | mergedDefinitions.addAll(mergedFragments.values()); 39 | 40 | return Document.newDocument().definitions(mergedDefinitions).build(); 41 | } 42 | 43 | private OperationDefinition mergeOperationDefinitions(OperationDefinition op1, OperationDefinition op2) { 44 | SelectionSet mergedSelectionSet = mergeSelectionSets(op1.getSelectionSet(), op2.getSelectionSet()); 45 | 46 | return OperationDefinition.newOperationDefinition() 47 | .name(op1.getName()) // Use the first operation name 48 | .operation(op1.getOperation()) 49 | .variableDefinitions(mergeVariableDefinitions(op1.getVariableDefinitions(), op2.getVariableDefinitions())) 50 | .selectionSet(mergedSelectionSet) 51 | .build(); 52 | } 53 | 54 | 55 | private FragmentDefinition mergeFragmentDefinitions(FragmentDefinition frag1, FragmentDefinition frag2) { 56 | SelectionSet mergedSelectionSet = mergeSelectionSets(frag1.getSelectionSet(), frag2.getSelectionSet()); 57 | 58 | return FragmentDefinition.newFragmentDefinition() 59 | .name(frag1.getName()) 60 | .typeCondition(frag1.getTypeCondition()) 61 | .selectionSet(mergedSelectionSet) 62 | .build(); 63 | } 64 | 65 | private SelectionSet mergeSelectionSets(SelectionSet set1, SelectionSet set2) { 66 | if (set1 == null) return set2; 67 | if (set2 == null) return set1; 68 | 69 | Map mergedFields = new LinkedHashMap<>(); 70 | Map mergedFragments = new LinkedHashMap<>(); 71 | List> mergedSelections = new ArrayList<>(); 72 | 73 | // Process first set 74 | for (Selection selection : set1.getSelections()) { 75 | if (selection instanceof Field field) { 76 | mergedFields.put(field.getName(), field); 77 | } else if (selection instanceof FragmentSpread fragment) { 78 | mergedFragments.put(fragment.getName(), fragment); 79 | } else { 80 | mergedSelections.add(selection); 81 | } 82 | } 83 | 84 | // Process second set 85 | for (Selection selection : set2.getSelections()) { 86 | if (selection instanceof Field field) { 87 | mergedFields.merge(field.getName(), field, this::mergeFields); 88 | } else if (selection instanceof FragmentSpread fragment) { 89 | mergedFragments.putIfAbsent(fragment.getName(), fragment); // Ensure uniqueness 90 | } else if (!mergedSelections.contains(selection)) { 91 | mergedSelections.add(selection); 92 | } 93 | } 94 | 95 | mergedSelections.addAll(mergedFields.values()); 96 | mergedSelections.addAll(mergedFragments.values()); // Add unique fragment spreads 97 | 98 | return SelectionSet.newSelectionSet().selections(mergedSelections).build(); 99 | } 100 | 101 | private Field mergeFields(Field field1, Field field2) { 102 | SelectionSet mergedSelectionSet = mergeSelectionSets(field1.getSelectionSet(), field2.getSelectionSet()); 103 | 104 | return Field.newField() 105 | .name(field1.getName()) 106 | .alias(field1.getAlias()) 107 | .arguments(field1.getArguments().isEmpty() ? field2.getArguments() : field1.getArguments()) // Fix here 108 | .selectionSet(mergedSelectionSet) 109 | .build(); 110 | } 111 | 112 | 113 | private List mergeVariableDefinitions(List vars1, List vars2) { 114 | Map mergedVars = new LinkedHashMap<>(); 115 | 116 | for (VariableDefinition var : vars1) { 117 | mergedVars.put(var.getName(), var); 118 | } 119 | 120 | for (VariableDefinition var : vars2) { 121 | mergedVars.putIfAbsent(var.getName(), var); 122 | } 123 | 124 | return new ArrayList<>(mergedVars.values()); 125 | } 126 | } 127 | 128 | -------------------------------------------------------------------------------- /src/main/java/com/pdstat/gqlextractor/service/GqlExtractorOutputHandlerService.java: -------------------------------------------------------------------------------- 1 | package com.pdstat.gqlextractor.service; 2 | 3 | import com.pdstat.gqlextractor.Constants; 4 | import com.pdstat.gqlextractor.model.OutputMode; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.boot.ApplicationArguments; 8 | import org.springframework.stereotype.Service; 9 | 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | 13 | @Service 14 | public class GqlExtractorOutputHandlerService { 15 | 16 | private static final Logger logger = LoggerFactory.getLogger(GqlExtractorOutputHandlerService.class); 17 | 18 | private static final int DEFAULT_MAX_DEPTH = 10; 19 | 20 | private final GqlFieldWordListWriterService gqlFieldWordListWriterService; 21 | private final GqlOperationFilesWriterService gqlOperationFilesWriterService; 22 | private final GqlJsonRequestFileWriterService gqlJsonRequestFileWriterService; 23 | private final GqlSchemaFieldPathWriterService gqlSchemaFieldPathWriterService; 24 | private final GqlFieldPathWriterService gqlFieldPathWriterService; 25 | private final ApplicationArguments appArgs; 26 | 27 | public GqlExtractorOutputHandlerService(GqlFieldWordListWriterService gqlFieldWordListWriterService, 28 | GqlOperationFilesWriterService gqlOperationFilesWriterService, 29 | GqlJsonRequestFileWriterService gqlJsonRequestFileWriterService, 30 | GqlSchemaFieldPathWriterService gqlSchemaFieldPathWriterService, 31 | GqlFieldPathWriterService gqlFieldPathWriterService, 32 | ApplicationArguments appArgs) { 33 | this.gqlFieldWordListWriterService = gqlFieldWordListWriterService; 34 | this.gqlOperationFilesWriterService = gqlOperationFilesWriterService; 35 | this.gqlJsonRequestFileWriterService = gqlJsonRequestFileWriterService; 36 | this.gqlSchemaFieldPathWriterService = gqlSchemaFieldPathWriterService; 37 | this.gqlFieldPathWriterService = gqlFieldPathWriterService; 38 | this.appArgs = appArgs; 39 | } 40 | 41 | public void handleGQLExtractorOutput() { 42 | String outputDirectory = appArgs.getOptionValues(Constants.Arguments.OUTPUT_DIRECTORY).get(0); 43 | 44 | if (schemaInputProvided()) { 45 | handleSchemaFieldNameSearch(outputDirectory); 46 | } else { 47 | List outputModes = getOutputModes(); 48 | 49 | if (outputModes.contains(OutputMode.ALL)) { 50 | gqlFieldWordListWriterService.writeFieldsFile(outputDirectory); 51 | gqlOperationFilesWriterService.writeOperationFiles(outputDirectory); 52 | gqlJsonRequestFileWriterService.writeJsonRequestFiles(outputDirectory); 53 | handleOperationsFieldSearch(outputDirectory); 54 | } else { 55 | if (outputModes.contains(OutputMode.FIELDS)) { 56 | gqlFieldWordListWriterService.writeFieldsFile(outputDirectory); 57 | } 58 | 59 | if (outputModes.contains(OutputMode.OPERATIONS)) { 60 | gqlOperationFilesWriterService.writeOperationFiles(outputDirectory); 61 | } 62 | 63 | if (outputModes.contains(OutputMode.REQUESTS)) { 64 | gqlJsonRequestFileWriterService.writeJsonRequestFiles(outputDirectory); 65 | } 66 | 67 | if (outputModes.contains(OutputMode.PATHS)) { 68 | handleOperationsFieldSearch(outputDirectory); 69 | } 70 | } 71 | } 72 | 73 | } 74 | 75 | private void handleOperationsFieldSearch(String outputDirectory) { 76 | checkSearchFieldProvided(); 77 | String searchField = appArgs.getOptionValues(Constants.Arguments.SEARCH_FIELD).get(0); 78 | gqlFieldPathWriterService.writeFieldsReport(outputDirectory, searchField); 79 | } 80 | 81 | private void handleSchemaFieldNameSearch(String outputDirectory) { 82 | checkSearchFieldProvided(); 83 | int maxDepth = DEFAULT_MAX_DEPTH; 84 | if (appArgs.containsOption(Constants.Arguments.DEPTH)) { 85 | try { 86 | maxDepth = Integer.parseInt(appArgs.getOptionValues(Constants.Arguments.DEPTH).get(0)); 87 | } catch (NumberFormatException e) { 88 | logger.warn("Invalid depth value provided, using default value of {}", DEFAULT_MAX_DEPTH); 89 | } 90 | } 91 | 92 | String inputSchema = appArgs.getOptionValues(Constants.Arguments.INPUT_SCHEMA).get(0); 93 | String searchField = appArgs.getOptionValues(Constants.Arguments.SEARCH_FIELD).get(0); 94 | gqlSchemaFieldPathWriterService.writeSchemaFieldPaths(outputDirectory, inputSchema, searchField, maxDepth); 95 | } 96 | 97 | private void checkSearchFieldProvided() { 98 | if (!appArgs.containsOption(Constants.Arguments.SEARCH_FIELD)) { 99 | logger.error("--search-field not provided. Exiting application. Use the --help option for more information."); 100 | System.exit(1); 101 | } 102 | } 103 | 104 | private boolean schemaInputProvided() { 105 | return appArgs.containsOption(Constants.Arguments.INPUT_SCHEMA); 106 | } 107 | 108 | private List getOutputModes() { 109 | List outputModes = new ArrayList<>(); 110 | List selectedOutputModes = appArgs.getOptionValues(Constants.Arguments.OUTPUT_MODE); 111 | for (String selectedOutputMode : selectedOutputModes) { 112 | outputModes.add(OutputMode.fromMode(selectedOutputMode)); 113 | } 114 | 115 | if (outputModes.isEmpty()) { 116 | outputModes.add(OutputMode.REQUESTS); 117 | } 118 | return outputModes; 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /src/main/java/com/pdstat/gqlextractor/repo/GqlDocumentRepository.java: -------------------------------------------------------------------------------- 1 | package com.pdstat.gqlextractor.repo; 2 | 3 | import com.pdstat.gqlextractor.Constants; 4 | import com.pdstat.gqlextractor.extractor.GqlDocumentExtractor; 5 | import graphql.language.Document; 6 | import graphql.parser.InvalidSyntaxException; 7 | import graphql.parser.Parser; 8 | import graphql.parser.ParserOptions; 9 | import jakarta.annotation.PostConstruct; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | import org.springframework.boot.ApplicationArguments; 13 | import org.springframework.stereotype.Repository; 14 | import org.springframework.web.reactive.function.client.WebClient; 15 | 16 | import java.io.IOException; 17 | import java.nio.file.Files; 18 | import java.nio.file.Path; 19 | import java.nio.file.Paths; 20 | import java.util.ArrayList; 21 | import java.util.List; 22 | import java.util.concurrent.*; 23 | import java.util.stream.Stream; 24 | 25 | @Repository 26 | public class GqlDocumentRepository { 27 | 28 | private static final Logger logger = LoggerFactory.getLogger(GqlDocumentRepository.class); 29 | 30 | private static final String JS_EXTENSION = ".js"; 31 | private static final String GQL_EXTENSION = ".graphql"; 32 | 33 | private final ApplicationArguments appArgs; 34 | private final GqlDocumentExtractor gqlDocumentExtractor; 35 | private final List gqlDocuments = new ArrayList<>(); 36 | 37 | public GqlDocumentRepository(ApplicationArguments appArgs, GqlDocumentExtractor gqlDocumentExtractor) { 38 | this.appArgs = appArgs; 39 | this.gqlDocumentExtractor = gqlDocumentExtractor; 40 | } 41 | 42 | @PostConstruct 43 | void initGqlDocuments() { 44 | if (gqlDocuments.isEmpty()) { 45 | int coreThreads = Math.max(2, Runtime.getRuntime().availableProcessors()); 46 | int maxThreads = coreThreads * 4; 47 | ExecutorService executorService = new ThreadPoolExecutor( 48 | coreThreads, 49 | maxThreads, 50 | 60L, TimeUnit.SECONDS, 51 | new LinkedBlockingQueue<>() 52 | ); 53 | 54 | List> futures = new ArrayList<>(); 55 | 56 | if (appArgs.containsOption(Constants.Arguments.INPUT_DIRECTORY) || 57 | appArgs.containsOption(Constants.Arguments.INPUT_URLS)) { 58 | try { 59 | logger.info("Scanning for GraphQL Documents"); 60 | if (appArgs.containsOption(Constants.Arguments.INPUT_DIRECTORY)) { 61 | String scanDirectory = appArgs.getOptionValues(Constants.Arguments.INPUT_DIRECTORY).get(0); 62 | try (Stream paths = Files.walk(Paths.get(scanDirectory))) { 63 | paths.filter(Files::isRegularFile) 64 | .forEach(filePath -> futures.add(executorService.submit(() -> 65 | processJavascriptFile(filePath.toString())))); 66 | } catch (IOException e) { 67 | logger.error("Error reading directory: {}", scanDirectory, e); 68 | } 69 | } 70 | 71 | if (appArgs.containsOption(Constants.Arguments.INPUT_URLS)) { 72 | String inputUrls = appArgs.getOptionValues(Constants.Arguments.INPUT_URLS).get(0); 73 | try (Stream urls = Files.lines(Paths.get(inputUrls))) { 74 | urls.forEach(url -> futures.add(executorService.submit(() -> processUrl(url)))); 75 | } catch (IOException e) { 76 | logger.error("Error reading input urls: {}", inputUrls, e); 77 | } 78 | } 79 | 80 | for (Future future : futures) { 81 | try { 82 | future.get(); // Blocks until task completes 83 | } catch (InterruptedException | ExecutionException e) { 84 | logger.error("Error processing file in thread pool", e); 85 | } 86 | } 87 | } finally { 88 | executorService.shutdown(); 89 | } 90 | } else if (appArgs.containsOption(Constants.Arguments.INPUT_OPERATIONS)) { 91 | String scanDirectory = appArgs.getOptionValues(Constants.Arguments.INPUT_OPERATIONS).get(0); 92 | try (Stream paths = Files.walk(Paths.get(scanDirectory))) { 93 | paths.filter(Files::isRegularFile) 94 | .forEach(filePath -> processGqlOperationsFile(filePath.toString())); 95 | } catch (IOException e) { 96 | logger.error("Error reading directory: {}", scanDirectory, e); 97 | } 98 | } 99 | 100 | } 101 | } 102 | 103 | public List getGqlDocuments() { 104 | return gqlDocuments; 105 | } 106 | 107 | private void processUrl(String url) { 108 | try { 109 | logger.info("Processing URL: {}", url); 110 | WebClient client = WebClient.builder().codecs(configurer -> configurer.defaultCodecs() 111 | .maxInMemorySize(20 * 1024 * 1024)).baseUrl(url).build(); 112 | String content = client.get().retrieve().bodyToMono(String.class).block(); 113 | gqlDocuments.addAll(gqlDocumentExtractor.extract(content)); 114 | } catch (Exception e) { 115 | logger.error("Error reading URL: {}", url, e); 116 | } 117 | } 118 | 119 | private void processGqlOperationsFile(String filePath) { 120 | if (filePath.endsWith(GQL_EXTENSION)) { 121 | Path gqlFilePath = Paths.get(filePath); 122 | logger.info("Processing graphql operation file: {}", gqlFilePath.getFileName()); 123 | try { 124 | String content = Files.readString(gqlFilePath); 125 | ParserOptions options = ParserOptions.newParserOptions() 126 | .maxTokens(Integer.MAX_VALUE) // Disable limit (or set a higher value) 127 | .build(); 128 | 129 | ParserOptions.setDefaultParserOptions(options); 130 | ParserOptions.setDefaultOperationParserOptions(options); 131 | Document document = Parser.parse(content); 132 | gqlDocuments.add(document); 133 | } catch (IOException | InvalidSyntaxException e) { 134 | logger.error("Error reading graphql operation file: {}", gqlFilePath.getFileName(), e); 135 | } 136 | 137 | } 138 | } 139 | 140 | private void processJavascriptFile(String filePath) { 141 | if (filePath.endsWith(JS_EXTENSION)) { 142 | Path jsFilePath = Paths.get(filePath); 143 | logger.info("Processing javascript file: {}", jsFilePath.getFileName()); 144 | try { 145 | String content = Files.readString(jsFilePath); 146 | gqlDocuments.addAll(gqlDocumentExtractor.extract(content)); 147 | } catch (IOException e) { 148 | logger.error("Error reading javascript file: {}", jsFilePath.getFileName(), e); 149 | } 150 | } 151 | } 152 | 153 | } 154 | -------------------------------------------------------------------------------- /src/main/java/com/pdstat/gqlextractor/repo/GqlSchemaRepository.java: -------------------------------------------------------------------------------- 1 | package com.pdstat.gqlextractor.repo; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.core.type.TypeReference; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import com.pdstat.gqlextractor.Constants; 7 | import graphql.introspection.IntrospectionResultToSchema; 8 | import graphql.language.Document; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import org.springframework.boot.ApplicationArguments; 12 | import org.springframework.http.HttpHeaders; 13 | import org.springframework.http.MediaType; 14 | import org.springframework.http.ResponseEntity; 15 | import org.springframework.stereotype.Repository; 16 | import org.springframework.web.reactive.function.client.WebClient; 17 | 18 | import java.net.MalformedURLException; 19 | import java.net.URL; 20 | import java.nio.file.Files; 21 | import java.nio.file.Path; 22 | import java.util.HashMap; 23 | import java.util.List; 24 | import java.util.Map; 25 | 26 | @Repository 27 | public class GqlSchemaRepository { 28 | 29 | private static final Logger logger = LoggerFactory.getLogger(GqlSchemaRepository.class); 30 | 31 | private static final String INTROSPECTION_QUERY = "query IntrospectionQuery { __schema { queryType { name } " + 32 | "mutationType { name } subscriptionType { name } types { ...FullType } directives { name description " + 33 | "locations args { ...InputValue } } } } fragment FullType on __Type { kind name description " + 34 | "fields(includeDeprecated: true) { name description args { ...InputValue } type { ...TypeRef } " + 35 | "isDeprecated deprecationReason } inputFields { ...InputValue } interfaces { ...TypeRef } " + 36 | "enumValues(includeDeprecated: true) { name description isDeprecated deprecationReason } possibleTypes " + 37 | "{ ...TypeRef }} fragment InputValue on __InputValue { name description type { ...TypeRef } defaultValue}" + 38 | " fragment TypeRef on __Type { kind name ofType { kind name ofType { kind name ofType { kind name ofType " + 39 | "{ kind name ofType { kind name ofType { kind name ofType { kind name } } } } } } }}"; 40 | 41 | private Document gqlSchema; 42 | 43 | private final ObjectMapper mapper; 44 | private final ApplicationArguments appArgs; 45 | 46 | public GqlSchemaRepository(ObjectMapper mapper, ApplicationArguments appArgs) { 47 | this.mapper = mapper; 48 | this.appArgs = appArgs; 49 | } 50 | 51 | public Document getGqlSchema(String inputSchema) { 52 | if (gqlSchema == null) { 53 | try { 54 | handleRemoteIntrospectionSchema(inputSchema); 55 | } catch (MalformedURLException e) { 56 | handleLocalIntrospectionSchema(inputSchema); 57 | } 58 | } 59 | return gqlSchema; 60 | } 61 | 62 | private void handleLocalIntrospectionSchema(String inputSchema) { 63 | Path schemaFilePath = Path.of(inputSchema); 64 | if (schemaFilePath.toFile().exists()) { 65 | try { 66 | String schema = new String(Files.readAllBytes(schemaFilePath)); 67 | Map responseMap = mapper.readValue(schema, new TypeReference>() { 68 | }); 69 | if (responseMap.containsKey("data") && responseMap.get("data") != null) { 70 | initGqlSchemaDocument(responseMap); 71 | } else { 72 | logger.error("Invalid schema file: {}", inputSchema); 73 | System.exit(1); 74 | } 75 | } catch (Exception e) { 76 | logger.error("Error reading schema file", e); 77 | System.exit(1); 78 | } 79 | } else { 80 | logger.error("Schema file does not exist: {}", inputSchema); 81 | System.exit(1); 82 | } 83 | } 84 | 85 | private void handleRemoteIntrospectionSchema(String inputSchema) throws MalformedURLException { 86 | new URL(inputSchema); 87 | WebClient client = WebClient.builder().codecs(configurer -> configurer.defaultCodecs() 88 | .maxInMemorySize(20 * 1024 * 1024)) 89 | .baseUrl(inputSchema).build(); 90 | ResponseEntity response = client.post() 91 | .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) 92 | .headers(httpHeaders -> initialiseRequestHeaders().forEach(httpHeaders::add)) 93 | .bodyValue("{\"query\":\"" + INTROSPECTION_QUERY + "\"}") 94 | .retrieve().toEntity(String.class).block(); 95 | if (response != null && response.getStatusCode().is2xxSuccessful()) { 96 | try { 97 | TypeReference> typeRef 98 | = new TypeReference>() { 99 | }; 100 | Map responseMap = mapper.readValue(response.getBody(), typeRef); 101 | if (responseMap.containsKey("errors")) { 102 | List> errors = (List>) responseMap.get("errors"); 103 | if (errors.isEmpty()) { 104 | logger.error("Introspection query failed"); 105 | } else { 106 | Map error = errors.get(0); 107 | logger.error("Introspection query failed: {}", mapper.writeValueAsString(error)); 108 | } 109 | System.exit(1); 110 | } else if (responseMap.containsKey("data")) { 111 | initGqlSchemaDocument(responseMap); 112 | } else { 113 | logger.error("Introspection query failed"); 114 | System.exit(1); 115 | } 116 | } catch (JsonProcessingException e) { 117 | logger.error("Introspection query failed: {}", e.getMessage(), e); 118 | System.exit(1); 119 | } 120 | } else { 121 | logger.error("Introspection query failed, server responded with status code: {}", 122 | response != null ? response.getStatusCode() : "null"); 123 | System.exit(1); 124 | } 125 | } 126 | 127 | private void initGqlSchemaDocument(Map responseMap) { 128 | Map data = (Map) responseMap.get("data"); 129 | IntrospectionResultToSchema introspectionResultToSchema = new IntrospectionResultToSchema(); 130 | gqlSchema = introspectionResultToSchema.createSchemaDefinition(data); 131 | } 132 | 133 | private Map initialiseRequestHeaders() { 134 | Map headers = new HashMap<>(); 135 | if (appArgs.containsOption(Constants.Arguments.REQUEST_HEADER)) { 136 | List requestHeaders = appArgs.getOptionValues(Constants.Arguments.REQUEST_HEADER); 137 | for (String requestHeader : requestHeaders) { 138 | String[] header = requestHeader.split(":"); 139 | if (header.length == 2) { 140 | headers.put(header[0].trim(), header[1].trim()); 141 | } 142 | } 143 | } 144 | return headers; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | <# : batch portion 2 | @REM ---------------------------------------------------------------------------- 3 | @REM Licensed to the Apache Software Foundation (ASF) under one 4 | @REM or more contributor license agreements. See the NOTICE file 5 | @REM distributed with this work for additional information 6 | @REM regarding copyright ownership. The ASF licenses this file 7 | @REM to you under the Apache License, Version 2.0 (the 8 | @REM "License"); you may not use this file except in compliance 9 | @REM with the License. You may obtain a copy of the License at 10 | @REM 11 | @REM http://www.apache.org/licenses/LICENSE-2.0 12 | @REM 13 | @REM Unless required by applicable law or agreed to in writing, 14 | @REM software distributed under the License is distributed on an 15 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | @REM KIND, either express or implied. See the License for the 17 | @REM specific language governing permissions and limitations 18 | @REM under the License. 19 | @REM ---------------------------------------------------------------------------- 20 | 21 | @REM ---------------------------------------------------------------------------- 22 | @REM Apache Maven Wrapper startup batch script, version 3.3.2 23 | @REM 24 | @REM Optional ENV vars 25 | @REM MVNW_REPOURL - repo url base for downloading maven distribution 26 | @REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 27 | @REM MVNW_VERBOSE - true: enable verbose log; others: silence the output 28 | @REM ---------------------------------------------------------------------------- 29 | 30 | @IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) 31 | @SET __MVNW_CMD__= 32 | @SET __MVNW_ERROR__= 33 | @SET __MVNW_PSMODULEP_SAVE=%PSModulePath% 34 | @SET PSModulePath= 35 | @FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( 36 | IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) 37 | ) 38 | @SET PSModulePath=%__MVNW_PSMODULEP_SAVE% 39 | @SET __MVNW_PSMODULEP_SAVE= 40 | @SET __MVNW_ARG0_NAME__= 41 | @SET MVNW_USERNAME= 42 | @SET MVNW_PASSWORD= 43 | @IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) 44 | @echo Cannot start maven from wrapper >&2 && exit /b 1 45 | @GOTO :EOF 46 | : end batch / begin powershell #> 47 | 48 | $ErrorActionPreference = "Stop" 49 | if ($env:MVNW_VERBOSE -eq "true") { 50 | $VerbosePreference = "Continue" 51 | } 52 | 53 | # calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties 54 | $distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl 55 | if (!$distributionUrl) { 56 | Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" 57 | } 58 | 59 | switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { 60 | "maven-mvnd-*" { 61 | $USE_MVND = $true 62 | $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" 63 | $MVN_CMD = "mvnd.cmd" 64 | break 65 | } 66 | default { 67 | $USE_MVND = $false 68 | $MVN_CMD = $script -replace '^mvnw','mvn' 69 | break 70 | } 71 | } 72 | 73 | # apply MVNW_REPOURL and calculate MAVEN_HOME 74 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 75 | if ($env:MVNW_REPOURL) { 76 | $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } 77 | $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" 78 | } 79 | $distributionUrlName = $distributionUrl -replace '^.*/','' 80 | $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' 81 | $MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" 82 | if ($env:MAVEN_USER_HOME) { 83 | $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" 84 | } 85 | $MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' 86 | $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" 87 | 88 | if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { 89 | Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" 90 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 91 | exit $? 92 | } 93 | 94 | if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { 95 | Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" 96 | } 97 | 98 | # prepare tmp dir 99 | $TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile 100 | $TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" 101 | $TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null 102 | trap { 103 | if ($TMP_DOWNLOAD_DIR.Exists) { 104 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 105 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 106 | } 107 | } 108 | 109 | New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null 110 | 111 | # Download and Install Apache Maven 112 | Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 113 | Write-Verbose "Downloading from: $distributionUrl" 114 | Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 115 | 116 | $webclient = New-Object System.Net.WebClient 117 | if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { 118 | $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) 119 | } 120 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 121 | $webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null 122 | 123 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 124 | $distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum 125 | if ($distributionSha256Sum) { 126 | if ($USE_MVND) { 127 | Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." 128 | } 129 | Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash 130 | if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { 131 | Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." 132 | } 133 | } 134 | 135 | # unzip and move 136 | Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null 137 | Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null 138 | try { 139 | Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null 140 | } catch { 141 | if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { 142 | Write-Error "fail to move MAVEN_HOME" 143 | } 144 | } finally { 145 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 146 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 147 | } 148 | 149 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 150 | -------------------------------------------------------------------------------- /src/test/java/com/pdstat/gqlextractor/model/GqlRequestTest.java: -------------------------------------------------------------------------------- 1 | package com.pdstat.gqlextractor.model; 2 | 3 | import com.pdstat.gqlextractor.repo.DefaultParamsRepository; 4 | import org.junit.jupiter.api.Test; 5 | import org.junit.jupiter.api.extension.ExtendWith; 6 | import org.mockito.Mock; 7 | import org.mockito.junit.jupiter.MockitoExtension; 8 | 9 | import static org.junit.jupiter.api.Assertions.assertEquals; 10 | import static org.junit.jupiter.api.Assertions.assertTrue; 11 | 12 | @ExtendWith(MockitoExtension.class) 13 | public class GqlRequestTest { 14 | 15 | @Mock 16 | private DefaultParamsRepository defaultParamsRepository; 17 | 18 | @Test 19 | void testGqlRequestNoParams() { 20 | // GqlRequest gqlRequest = new GqlRequest("CountriesSupported", "fragment CountryFields on Country {\n" + 21 | // " code\n" + 22 | // " name\n" + 23 | // " mandatoryPostalCode\n" + 24 | // " regions {\n" + 25 | // " code\n" + 26 | // " name\n" + 27 | // " }\n" + 28 | // "}\n" + 29 | // "query CountriesSupported {\n" + 30 | // " bssConfiguration {\n" + 31 | // " addressFormMode\n" + 32 | // " shippingCountries: supportedCountries(context: \"shipping\") {\n" + 33 | // " ...CountryFields\n" + 34 | // " }\n" + 35 | // " billingCountries: supportedCountries(context: \"billing\") {\n" + 36 | // " ...CountryFields\n" + 37 | // " }\n" + 38 | // " }\n" + 39 | // "}", defaultParamsRepository); 40 | // assertEquals("fragment CountryFields on Country {\n code\n name\n mandatoryPostalCode\n regions {" + 41 | // "\n code\n name\n }\n}\nquery CountriesSupported {\n bssConfiguration {\n addressFormMode\n" + 42 | // " shippingCountries: supportedCountries(context: \"shipping\") {\n ...CountryFields\n }\n" + 43 | // " billingCountries: supportedCountries(context: \"billing\") {\n ...CountryFields\n }\n }\n}", 44 | // gqlRequest.getQuery()); 45 | // assertEquals("CountriesSupported", gqlRequest.getOperationName()); 46 | // assertEquals(0, gqlRequest.getVariables().size()); 47 | } 48 | 49 | @Test 50 | void testGqlRequestOneParam() { 51 | // GqlRequest gqlRequest = new GqlRequest("ConnectBssToLyft", "mutation ConnectBssToLyft($jwt: String!) {\n" + 52 | // " connectBssToLyft(jwt: $jwt) {\n" + 53 | // " id\n" + 54 | // " hasLinkedBssAccount\n" + 55 | // " migration {\n" + 56 | // " id\n" + 57 | // " needsToSeePrompt\n" + 58 | // " }\n" + 59 | // " termsToAgreeTo {\n" + 60 | // " termsId\n" + 61 | // " }\n" + 62 | // " }\n" + 63 | // "}", defaultParamsRepository); 64 | // assertEquals("mutation ConnectBssToLyft($jwt: String!) {\n connectBssToLyft(jwt: $jwt) {\n id\n" + 65 | // " hasLinkedBssAccount\n migration {\n id\n needsToSeePrompt\n }\n" + 66 | // " termsToAgreeTo {\n termsId\n }\n }\n}", 67 | // gqlRequest.getQuery()); 68 | // assertEquals("ConnectBssToLyft", gqlRequest.getOperationName()); 69 | // assertEquals(1, gqlRequest.getVariables().size()); 70 | // assertTrue(gqlRequest.getVariables().containsKey("jwt")); 71 | // assertEquals("", gqlRequest.getVariables().get("jwt")); 72 | } 73 | 74 | @Test 75 | void testGqlRequestMultipleParams() { 76 | // GqlRequest gqlRequest = new GqlRequest("BssPurchasePageCpaPayload", "query BssPurchasePageCpaPayload($params: SubscriptionTypesParams, $queryString: String, $autoRenew: Boolean, $memberId: Int) {\n" + 77 | // " currentMarket {\n" + 78 | // " subscriptionTypes(params: $params, memberId: $memberId) {\n" + 79 | // " id\n" + 80 | // " quotation(params: $params) {\n" + 81 | // " id\n" + 82 | // " payloadForCpaShield(\n" + 83 | // " purchasePageData: {autoRenew: $autoRenew, queryString: $queryString}\n" + 84 | // " )\n" + 85 | // " }\n" + 86 | // " }\n" + 87 | // " }\n" + 88 | // "}", defaultParamsRepository); 89 | // assertEquals("query BssPurchasePageCpaPayload($params: SubscriptionTypesParams, $queryString: String, " + 90 | // "$autoRenew: Boolean, $memberId: Int) {\n currentMarket {\n " + 91 | // "subscriptionTypes(params: $params, memberId: $memberId) {\n id\n " + 92 | // "quotation(params: $params) {\n id\n payloadForCpaShield(\n " + 93 | // "purchasePageData: {autoRenew: $autoRenew, queryString: $queryString}\n )\n " + 94 | // "}\n }\n }\n}", 95 | // gqlRequest.getQuery()); 96 | // assertEquals("BssPurchasePageCpaPayload", gqlRequest.getOperationName()); 97 | // assertEquals(4, gqlRequest.getVariables().size()); 98 | // assertTrue(gqlRequest.getVariables().containsKey("params")); 99 | // assertTrue(gqlRequest.getVariables().get("params") instanceof Object); 100 | // assertTrue(gqlRequest.getVariables().containsKey("queryString")); 101 | // assertEquals("", gqlRequest.getVariables().get("queryString")); 102 | // assertTrue(gqlRequest.getVariables().containsKey("autoRenew")); 103 | // assertEquals(false, gqlRequest.getVariables().get("autoRenew")); 104 | // assertTrue(gqlRequest.getVariables().containsKey("memberId")); 105 | // assertEquals(0, gqlRequest.getVariables().get("memberId")); 106 | } 107 | 108 | @Test 109 | void testGqlRequestParamsNoSpaces() { 110 | // GqlRequest gqlRequest = new GqlRequest("AccountReonboardingRetrieveQuery", "query AccountReonboardingRetrieveQuery($input:OpenapiGetV1AccountInput" + 111 | // "$v1Context:V1ContextInput!)@tag(id:\\\"gql\\\")@hash(id:\\\"aad7352242\\\"){account:v1Account(input:" + 112 | // "$input v1Context:$v1Context)@params(expand:[]includeOnly:[\\\"id\\\" \\\"merchant_reonboarded_from\\\" " + 113 | // "\\\"merchants_reonboarded_to\\\"]fragments:[]nodeLookupMap:[{nodes:[\\\"id\\\" \\\"merchant_reonboarded_from\\\" " + 114 | // "\\\"merchants_reonboarded_to\\\"]parentPath:\\\"ROOT\\\"}]predicates:{})@rest(type:\\\"OpenapiAccount\\\" " + 115 | // "method:\\\"GET\\\" pathBuilder:$pathBuilder runtimePath:\\\"/v1/account\\\" queryName:\\\"account\\\")" + 116 | // "{id merchant_reonboarded_from merchants_reonboarded_to}}", defaultParamsRepository); 117 | // 118 | // assertEquals("AccountReonboardingRetrieveQuery", gqlRequest.getOperationName()); 119 | // assertEquals(2, gqlRequest.getVariables().size()); 120 | // assertTrue(gqlRequest.getVariables().containsKey("input")); 121 | // assertTrue(gqlRequest.getVariables().get("input") instanceof Object); 122 | // assertTrue(gqlRequest.getVariables().containsKey("v1Context")); 123 | // assertTrue(gqlRequest.getVariables().get("v1Context") instanceof Object); 124 | } 125 | 126 | } 127 | -------------------------------------------------------------------------------- /src/test/java/com/pdstat/gqlextractor/repo/GqlOperationsRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.pdstat.gqlextractor.repo; 2 | 3 | import static org.mockito.ArgumentMatchers.any; 4 | 5 | import com.pdstat.gqlextractor.service.GqlMergerService; 6 | import graphql.language.Document; 7 | import graphql.language.FragmentDefinition; 8 | import graphql.parser.Parser; 9 | import org.junit.jupiter.api.Assertions; 10 | import org.junit.jupiter.api.Test; 11 | import org.junit.jupiter.api.extension.ExtendWith; 12 | import org.mockito.InjectMocks; 13 | import org.mockito.Mock; 14 | import org.mockito.Mockito; 15 | import org.mockito.junit.jupiter.MockitoExtension; 16 | 17 | import java.util.List; 18 | 19 | @ExtendWith(MockitoExtension.class) 20 | public class GqlOperationsRepositoryTest { 21 | 22 | @Mock 23 | private GqlDocumentRepository gqlDocumentRepository; 24 | 25 | @Mock 26 | private GqlFragmentDefinitionsRepository gqlFragmentDefinitionsRepository; 27 | 28 | @Mock 29 | private GqlMergerService gqlMergerService; 30 | 31 | @InjectMocks 32 | private GqlOperationsRepository gqlOperationsRepository; 33 | 34 | @Test 35 | void testInitGqlOperationsMissingFragment() { 36 | String getSupplyQuery = "query GetSupply($input: SupplyInput) { supply(input: $input) { stations { stationId " + 37 | "stationName location { lat lng } bikesAvailable bikeDocksAvailable ebikesAvailable scootersAvailable " + 38 | "totalBikesAvailable totalRideablesAvailable isValet isOffline isLightweight notices { ...NoticeFields }" + 39 | "siteId ebikes { rideableName batteryStatus { distanceRemaining { value unit } percent } } scooters { " + 40 | "rideableName batteryStatus { distanceRemaining { value unit } percent } } lastUpdatedMs } rideables { " + 41 | "rideableId location { lat lng } rideableType photoUrl batteryStatus { distanceRemaining { value unit } " + 42 | "percent } } notices { ...NoticeFields } requestErrors { ...NoticeFields } } }"; 43 | String noticeFieldsFragment = "fragment NoticeFields on Notice { localizedTitle localizedDescription url }"; 44 | Document getSupplyDocument = new Parser().parseDocument(getSupplyQuery); 45 | Document noticeFieldsFragmentDocument = new Parser().parseDocument(noticeFieldsFragment); 46 | 47 | Mockito.when(gqlDocumentRepository.getGqlDocuments()).thenReturn(List.of(getSupplyDocument)); 48 | Mockito.when(gqlFragmentDefinitionsRepository.getGqlFragmentDefinition("NoticeFields")) 49 | .thenReturn((FragmentDefinition) noticeFieldsFragmentDocument.getDefinitions().get(0)); 50 | 51 | gqlOperationsRepository.initGqlOperations(); 52 | 53 | Document storedGetSupplyDoc = gqlOperationsRepository.getGqlOperation("GetSupply"); 54 | Assertions.assertNotNull(storedGetSupplyDoc); 55 | Assertions.assertTrue(storedGetSupplyDoc.getDefinitions().size() > 1); 56 | } 57 | 58 | @Test 59 | void testInitGqlOperationsMergeDocuments() { 60 | String getSupplyQuery1 = "query GetSupply($input: SupplyInput) { supply(input: $input) { stations { stationId " + 61 | "location { lat lng } bikesAvailable ebikesAvailable scootersAvailable " + 62 | "totalBikesAvailable totalRideablesAvailable isValet isOffline isLightweight notices { ...NoticeFields }" + 63 | "siteId ebikes { batteryStatus { distanceRemaining { unit } percent } } scooters { " + 64 | "batteryStatus { distanceRemaining { value unit } percent } } lastUpdatedMs } rideables { " + 65 | "location { lat } rideableType batteryStatus { distanceRemaining { value } " + 66 | "percent } } notices { ...NoticeFields } requestErrors { ...NoticeFields } } } fragment NoticeFields on" + 67 | " Notice { localizedTitle localizedDescription url }"; 68 | String getSupplyQuery2 = "query GetSupply($input: SupplyInput) { supply(input: $input) { stations { stationId " + 69 | "stationName location { lat lng } bikesAvailable bikeDocksAvailable ebikesAvailable " + 70 | "totalBikesAvailable totalRideablesAvailable isValet isOffline isLightweight notices { ...NoticeFields }" + 71 | "siteId ebikes { rideableName batteryStatus { distanceRemaining { value unit } percent } } scooters { " + 72 | "rideableName batteryStatus { distanceRemaining { value unit } percent } } lastUpdatedMs } rideables { " + 73 | "rideableId location { lat lng } rideableType photoUrl batteryStatus { distanceRemaining { value unit } " + 74 | "percent } } notices { ...NoticeFields } requestErrors { ...NoticeFields } } } fragment NoticeFields on" + 75 | " Notice { localizedTitle localizedDescription url }"; 76 | Document getSupplyDocument1 = new Parser().parseDocument(getSupplyQuery1); 77 | Document getSupplyDocument2 = new Parser().parseDocument(getSupplyQuery2); 78 | 79 | Mockito.when(gqlDocumentRepository.getGqlDocuments()).thenReturn(List.of(getSupplyDocument1, getSupplyDocument2)); 80 | Mockito.when(gqlMergerService.mergeGraphQLDocuments(any(), any())) 81 | .thenReturn(getSupplyDocument2); 82 | 83 | gqlOperationsRepository.initGqlOperations(); 84 | } 85 | 86 | @Test 87 | void testInitGqlOperationsNoMergeDocuments() { 88 | String getSupplyQuery1 = "query GetSupply($input: SupplyInput) { supply(input: $input) { stations { stationId " + 89 | "stationName location { lat lng } bikesAvailable bikeDocksAvailable ebikesAvailable " + 90 | "totalBikesAvailable totalRideablesAvailable isValet isOffline isLightweight notices { ...NoticeFields }" + 91 | "siteId ebikes { rideableName batteryStatus { distanceRemaining { value unit } percent } } scooters { " + 92 | "rideableName batteryStatus { distanceRemaining { value unit } percent } } lastUpdatedMs } rideables { " + 93 | "rideableId location { lat lng } rideableType photoUrl batteryStatus { distanceRemaining { value unit } " + 94 | "percent } } notices { ...NoticeFields } requestErrors { ...NoticeFields } } } fragment NoticeFields on" + 95 | " Notice { localizedTitle localizedDescription url }"; 96 | String getSupplyQuery2 = "query GetSupply($input: SupplyInput) { supply(input: $input) { stations { stationId " + 97 | "stationName location { lat lng } bikesAvailable bikeDocksAvailable ebikesAvailable " + 98 | "totalBikesAvailable totalRideablesAvailable isValet isOffline isLightweight notices { ...NoticeFields }" + 99 | "siteId ebikes { rideableName batteryStatus { distanceRemaining { value unit } percent } } scooters { " + 100 | "rideableName batteryStatus { distanceRemaining { value unit } percent } } lastUpdatedMs } rideables { " + 101 | "rideableId location { lat lng } rideableType photoUrl batteryStatus { distanceRemaining { value unit } " + 102 | "percent } } notices { ...NoticeFields } requestErrors { ...NoticeFields } } } fragment NoticeFields on" + 103 | " Notice { localizedTitle localizedDescription url }"; 104 | Document getSupplyDocument1 = new Parser().parseDocument(getSupplyQuery1); 105 | Document getSupplyDocument2 = new Parser().parseDocument(getSupplyQuery2); 106 | 107 | Mockito.when(gqlDocumentRepository.getGqlDocuments()).thenReturn(List.of(getSupplyDocument1, getSupplyDocument2)); 108 | 109 | gqlOperationsRepository.initGqlOperations(); 110 | 111 | Mockito.verify(gqlMergerService, Mockito.never()).mergeGraphQLDocuments(getSupplyDocument1, getSupplyDocument2); 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /src/test/java/com/pdstat/gqlextractor/service/GqlSchemaPathFinderTest.java: -------------------------------------------------------------------------------- 1 | package com.pdstat.gqlextractor.service; 2 | 3 | import graphql.language.Document; 4 | import graphql.parser.Parser; 5 | import org.junit.jupiter.api.Assertions; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import java.util.List; 9 | 10 | public class GqlSchemaPathFinderTest { 11 | 12 | @Test 13 | void testFindFieldPaths() { 14 | String schema = """ 15 | "" 16 | type Query { 17 | "Get a specific character by ID" 18 | character( 19 | "" 20 | id: ID!): Character 21 | "Get the list of all characters" 22 | characters( 23 | "" 24 | page: Int 25 | "" 26 | filter: FilterCharacter): Characters 27 | "Get a list of characters selected by ids" 28 | charactersByIds( 29 | "" 30 | ids: [ID!]!): [Character] 31 | "Get a specific locations by ID" 32 | location( 33 | "" 34 | id: ID!): Location 35 | "Get the list of all locations" 36 | locations( 37 | "" 38 | page: Int 39 | "" 40 | filter: FilterLocation): Locations 41 | "Get a list of locations selected by ids" 42 | locationsByIds( 43 | "" 44 | ids: [ID!]!): [Location] 45 | "Get a specific episode by ID" 46 | episode( 47 | "" 48 | id: ID!): Episode 49 | "Get the list of all episodes" 50 | episodes( 51 | "" 52 | page: Int 53 | "" 54 | filter: FilterEpisode): Episodes 55 | "Get a list of episodes selected by ids" 56 | episodesByIds( 57 | "" 58 | ids: [ID!]!): [Episode] 59 | } 60 | 61 | "" 62 | type Character { 63 | "The id of the character." 64 | id: ID 65 | "The name of the character." 66 | name: String 67 | "The status of the character ('Alive', 'Dead' or 'unknown')." 68 | status: String 69 | "The species of the character." 70 | species: String 71 | "The type or subspecies of the character." 72 | type: String 73 | "The gender of the character ('Female', 'Male', 'Genderless' or 'unknown')." 74 | gender: String 75 | "The character's origin location" 76 | origin: Location 77 | "The character's last known location" 78 | location: Location 79 | ""\" 80 | Link to the character's image. 81 | All images are 300x300px and most are medium shots or portraits since they are intended to be used as avatars. 82 | ""\" 83 | image: String 84 | "Episodes in which this character appeared." 85 | episode: [Episode]! 86 | "Time at which the character was created in the database." 87 | created: String 88 | } 89 | 90 | "" 91 | type Location { 92 | "The id of the location." 93 | id: ID 94 | "The name of the location." 95 | name: String 96 | "The type of the location." 97 | type: String 98 | "The dimension in which the location is located." 99 | dimension: String 100 | "List of characters who have been last seen in the location." 101 | residents: [Character]! 102 | "Time at which the location was created in the database." 103 | created: String 104 | } 105 | 106 | "" 107 | type Episode { 108 | "The id of the episode." 109 | id: ID 110 | "The name of the episode." 111 | name: String 112 | "The air date of the episode." 113 | air_date: String 114 | "The code of the episode." 115 | episode: String 116 | "List of characters who have been seen in the episode." 117 | characters: [Character]! 118 | "Time at which the episode was created in the database." 119 | created: String 120 | } 121 | 122 | "" 123 | input FilterCharacter { 124 | "" 125 | name: String 126 | "" 127 | status: String 128 | "" 129 | species: String 130 | "" 131 | type: String 132 | "" 133 | gender: String 134 | } 135 | 136 | "" 137 | type Characters { 138 | "" 139 | info: Info 140 | "" 141 | results: [Character] 142 | } 143 | 144 | "" 145 | type Info { 146 | "The length of the response." 147 | count: Int 148 | "The amount of pages." 149 | pages: Int 150 | "Number of the next page (if it exists)" 151 | next: Int 152 | "Number of the previous page (if it exists)" 153 | prev: Int 154 | } 155 | 156 | "" 157 | input FilterLocation { 158 | "" 159 | name: String 160 | "" 161 | type: String 162 | "" 163 | dimension: String 164 | } 165 | 166 | "" 167 | type Locations { 168 | "" 169 | info: Info 170 | "" 171 | results: [Location] 172 | } 173 | 174 | "" 175 | input FilterEpisode { 176 | "" 177 | name: String 178 | "" 179 | episode: String 180 | } 181 | 182 | "" 183 | type Episodes { 184 | "" 185 | info: Info 186 | "" 187 | results: [Episode] 188 | } 189 | 190 | "" 191 | enum CacheControlScope { 192 | "" 193 | PUBLIC 194 | "" 195 | PRIVATE 196 | } 197 | 198 | "The `Upload` scalar type represents a file upload." 199 | scalar Upload 200 | 201 | "" 202 | directive @cacheControl("" 203 | maxAge: Int, "" 204 | scope: CacheControlScope) on FIELD_DEFINITION | OBJECT | INTERFACE 205 | """; 206 | 207 | Document document = new Parser().parseDocument(schema); 208 | 209 | GqlSchemaPathFinder finder = new GqlSchemaPathFinder(); 210 | List paths = finder.findFieldPaths(document, "name", 3); 211 | List expectedPaths = List.of( 212 | "name -> Character -> character -> Query", 213 | "name -> Location -> locationsByIds -> Query", 214 | "name -> Location -> location -> Query", 215 | "name -> Episode -> episode -> Query", 216 | "name -> Episode -> episodesByIds -> Query", 217 | "name -> Character -> charactersByIds -> Query" 218 | ); 219 | Assertions.assertEquals(expectedPaths, paths); 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/test/java/com/pdstat/gqlextractor/service/GqlRequestFactoryServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.pdstat.gqlextractor.service; 2 | 3 | import com.pdstat.gqlextractor.model.GqlRequest; 4 | import com.pdstat.gqlextractor.repo.DefaultParamsRepository; 5 | import graphql.language.Document; 6 | import graphql.parser.Parser; 7 | import org.junit.jupiter.api.Assertions; 8 | import org.junit.jupiter.api.Test; 9 | import org.junit.jupiter.api.extension.ExtendWith; 10 | import org.mockito.InjectMocks; 11 | import org.mockito.Mock; 12 | import org.mockito.Mockito; 13 | import org.mockito.junit.jupiter.MockitoExtension; 14 | 15 | import java.util.List; 16 | import java.util.Map; 17 | 18 | @ExtendWith(MockitoExtension.class) 19 | public class GqlRequestFactoryServiceTest { 20 | 21 | @Mock 22 | private DefaultParamsRepository defaultParamsRepository; 23 | 24 | @InjectMocks 25 | private GqlRequestFactoryService gqlRequestFactoryService; 26 | 27 | @Test 28 | void testCreateGqlRequestIntegerVariable() { 29 | // Create a GQL query string with an integer variable 30 | String gqlQuery = "query getPerson($id:Int!){person(id:$id){name}}"; 31 | Document document = new Parser().parseDocument(gqlQuery); 32 | 33 | Mockito.when(defaultParamsRepository.getDefaultParam("id")).thenReturn(null); 34 | 35 | GqlRequest gqlRequest = gqlRequestFactoryService.createGqlRequest(document); 36 | Assertions.assertEquals(gqlQuery, gqlRequest.getQuery()); 37 | Assertions.assertEquals("getPerson", gqlRequest.getOperationName()); 38 | Assertions.assertTrue(gqlRequest.getVariables().containsKey("id")); 39 | Assertions.assertEquals(0, gqlRequest.getVariables().get("id")); 40 | } 41 | 42 | @Test 43 | void testCreateGqlRequestIntegerListVariable() { 44 | // Create a GQL query string with an integer variable 45 | String gqlQuery = "query getPerson($id:[Int]!){person(id:$id){name}}"; 46 | Document document = new Parser().parseDocument(gqlQuery); 47 | 48 | Mockito.when(defaultParamsRepository.getDefaultParam("id")).thenReturn(null); 49 | 50 | GqlRequest gqlRequest = gqlRequestFactoryService.createGqlRequest(document); 51 | Assertions.assertEquals(gqlQuery, gqlRequest.getQuery()); 52 | Assertions.assertEquals("getPerson", gqlRequest.getOperationName()); 53 | Assertions.assertTrue(gqlRequest.getVariables().containsKey("id")); 54 | Assertions.assertEquals(List.of(0), gqlRequest.getVariables().get("id")); 55 | } 56 | 57 | @Test 58 | void testCreateGqlRequestFloatVariable() { 59 | // Create a GQL query string with an integer variable 60 | String gqlQuery = "query getPerson($id:Float!){person(id:$id){name}}"; 61 | Document document = new Parser().parseDocument(gqlQuery); 62 | 63 | Mockito.when(defaultParamsRepository.getDefaultParam("id")).thenReturn(null); 64 | 65 | GqlRequest gqlRequest = gqlRequestFactoryService.createGqlRequest(document); 66 | Assertions.assertEquals(gqlQuery, gqlRequest.getQuery()); 67 | Assertions.assertEquals("getPerson", gqlRequest.getOperationName()); 68 | Assertions.assertTrue(gqlRequest.getVariables().containsKey("id")); 69 | Assertions.assertEquals(0.0F, gqlRequest.getVariables().get("id")); 70 | } 71 | 72 | @Test 73 | void testCreateGqlRequestFloatListVariable() { 74 | // Create a GQL query string with an integer variable 75 | String gqlQuery = "query getPerson($id:[Float]!){person(id:$id){name}}"; 76 | Document document = new Parser().parseDocument(gqlQuery); 77 | 78 | Mockito.when(defaultParamsRepository.getDefaultParam("id")).thenReturn(null); 79 | 80 | GqlRequest gqlRequest = gqlRequestFactoryService.createGqlRequest(document); 81 | Assertions.assertEquals(gqlQuery, gqlRequest.getQuery()); 82 | Assertions.assertEquals("getPerson", gqlRequest.getOperationName()); 83 | Assertions.assertTrue(gqlRequest.getVariables().containsKey("id")); 84 | Assertions.assertEquals(List.of(0.0F), gqlRequest.getVariables().get("id")); 85 | } 86 | 87 | @Test 88 | void testCreateGqlRequestLongVariable() { 89 | // Create a GQL query string with an integer variable 90 | String gqlQuery = "query getPerson($id:Long!){person(id:$id){name}}"; 91 | Document document = new Parser().parseDocument(gqlQuery); 92 | 93 | Mockito.when(defaultParamsRepository.getDefaultParam("id")).thenReturn(null); 94 | 95 | GqlRequest gqlRequest = gqlRequestFactoryService.createGqlRequest(document); 96 | Assertions.assertEquals(gqlQuery, gqlRequest.getQuery()); 97 | Assertions.assertEquals("getPerson", gqlRequest.getOperationName()); 98 | Assertions.assertTrue(gqlRequest.getVariables().containsKey("id")); 99 | Assertions.assertEquals(0L, gqlRequest.getVariables().get("id")); 100 | } 101 | 102 | @Test 103 | void testCreateGqlRequestLongListVariable() { 104 | // Create a GQL query string with an integer variable 105 | String gqlQuery = "query getPerson($id:[Long]!){person(id:$id){name}}"; 106 | Document document = new Parser().parseDocument(gqlQuery); 107 | 108 | Mockito.when(defaultParamsRepository.getDefaultParam("id")).thenReturn(null); 109 | 110 | GqlRequest gqlRequest = gqlRequestFactoryService.createGqlRequest(document); 111 | Assertions.assertEquals(gqlQuery, gqlRequest.getQuery()); 112 | Assertions.assertEquals("getPerson", gqlRequest.getOperationName()); 113 | Assertions.assertTrue(gqlRequest.getVariables().containsKey("id")); 114 | Assertions.assertEquals(List.of(0L), gqlRequest.getVariables().get("id")); 115 | } 116 | 117 | @Test 118 | void testCreateGqlRequestBooleanVariable() { 119 | // Create a GQL query string with an integer variable 120 | String gqlQuery = "query getPerson($id:Boolean!){person(id:$id){name}}"; 121 | Document document = new Parser().parseDocument(gqlQuery); 122 | 123 | Mockito.when(defaultParamsRepository.getDefaultParam("id")).thenReturn(null); 124 | 125 | GqlRequest gqlRequest = gqlRequestFactoryService.createGqlRequest(document); 126 | Assertions.assertEquals(gqlQuery, gqlRequest.getQuery()); 127 | Assertions.assertEquals("getPerson", gqlRequest.getOperationName()); 128 | Assertions.assertTrue(gqlRequest.getVariables().containsKey("id")); 129 | Assertions.assertEquals(false, gqlRequest.getVariables().get("id")); 130 | } 131 | 132 | @Test 133 | void testCreateGqlRequestBooleanListVariable() { 134 | // Create a GQL query string with an integer variable 135 | String gqlQuery = "query getPerson($id:[Boolean]!){person(id:$id){name}}"; 136 | Document document = new Parser().parseDocument(gqlQuery); 137 | 138 | Mockito.when(defaultParamsRepository.getDefaultParam("id")).thenReturn(null); 139 | 140 | GqlRequest gqlRequest = gqlRequestFactoryService.createGqlRequest(document); 141 | Assertions.assertEquals(gqlQuery, gqlRequest.getQuery()); 142 | Assertions.assertEquals("getPerson", gqlRequest.getOperationName()); 143 | Assertions.assertTrue(gqlRequest.getVariables().containsKey("id")); 144 | Assertions.assertEquals(List.of(false), gqlRequest.getVariables().get("id")); 145 | } 146 | 147 | @Test 148 | void testCreateGqlRequestStringVariable() { 149 | // Create a GQL query string with an integer variable 150 | String gqlQuery = "query getPerson($id:String!){person(id:$id){name}}"; 151 | Document document = new Parser().parseDocument(gqlQuery); 152 | 153 | Mockito.when(defaultParamsRepository.getDefaultParam("id")).thenReturn(null); 154 | 155 | GqlRequest gqlRequest = gqlRequestFactoryService.createGqlRequest(document); 156 | Assertions.assertEquals(gqlQuery, gqlRequest.getQuery()); 157 | Assertions.assertEquals("getPerson", gqlRequest.getOperationName()); 158 | Assertions.assertTrue(gqlRequest.getVariables().containsKey("id")); 159 | Assertions.assertEquals("", gqlRequest.getVariables().get("id")); 160 | } 161 | 162 | @Test 163 | void testCreateGqlRequestStringListVariable() { 164 | // Create a GQL query string with an integer variable 165 | String gqlQuery = "query getPerson($id:[String]!){person(id:$id){name}}"; 166 | Document document = new Parser().parseDocument(gqlQuery); 167 | 168 | Mockito.when(defaultParamsRepository.getDefaultParam("id")).thenReturn(null); 169 | 170 | GqlRequest gqlRequest = gqlRequestFactoryService.createGqlRequest(document); 171 | Assertions.assertEquals(gqlQuery, gqlRequest.getQuery()); 172 | Assertions.assertEquals("getPerson", gqlRequest.getOperationName()); 173 | Assertions.assertTrue(gqlRequest.getVariables().containsKey("id")); 174 | Assertions.assertEquals(List.of(""), gqlRequest.getVariables().get("id")); 175 | } 176 | 177 | @Test 178 | void testCreateGqlRequestObjectVariable() { 179 | // Create a GQL query string with an integer variable 180 | String gqlQuery = "query getPerson($id:SomeObject!){person(id:$id){name}}"; 181 | Document document = new Parser().parseDocument(gqlQuery); 182 | 183 | Mockito.when(defaultParamsRepository.getDefaultParam("id")).thenReturn(null); 184 | 185 | GqlRequest gqlRequest = gqlRequestFactoryService.createGqlRequest(document); 186 | Assertions.assertEquals(gqlQuery, gqlRequest.getQuery()); 187 | Assertions.assertEquals("getPerson", gqlRequest.getOperationName()); 188 | Assertions.assertTrue(gqlRequest.getVariables().containsKey("id")); 189 | Assertions.assertInstanceOf(Map.class, gqlRequest.getVariables().get("id")); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/main/java/com/pdstat/gqlextractor/deserialiser/DocumentDeserialiser.java: -------------------------------------------------------------------------------- 1 | package com.pdstat.gqlextractor.deserialiser; 2 | 3 | import com.fasterxml.jackson.core.JsonParser; 4 | import com.fasterxml.jackson.core.JsonProcessingException; 5 | import com.fasterxml.jackson.core.type.TypeReference; 6 | import com.fasterxml.jackson.databind.DeserializationContext; 7 | import com.fasterxml.jackson.databind.JsonDeserializer; 8 | import com.fasterxml.jackson.databind.ObjectMapper; 9 | import graphql.language.*; 10 | 11 | import java.io.IOException; 12 | import java.math.BigDecimal; 13 | import java.util.ArrayList; 14 | import java.util.HashMap; 15 | import java.util.List; 16 | import java.util.Map; 17 | 18 | public class DocumentDeserialiser extends JsonDeserializer { 19 | 20 | @Override 21 | public Document deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException { 22 | ObjectMapper objectMapper = (ObjectMapper) jsonParser.getCodec(); 23 | Map node = objectMapper.readValue(jsonParser, new TypeReference>() { 24 | }); 25 | 26 | List definitions = new ArrayList<>(); 27 | List> definitionsJson = (List>) node.get("definitions"); 28 | 29 | for (Map def : definitionsJson) { 30 | if (namedNode(def)) { 31 | String kind = (String) def.get("kind"); 32 | 33 | if ("OperationDefinition".equals(kind)) { 34 | definitions.add(parseOperationDefinition(def)); 35 | } else if ("FragmentDefinition".equals(kind)) { 36 | definitions.add(parseFragmentDefinition(def)); 37 | } 38 | } 39 | } 40 | 41 | return Document.newDocument() 42 | .definitions(definitions) 43 | .build(); 44 | } 45 | 46 | private boolean namedNode(Map node) { 47 | if (node == null) { 48 | return false; 49 | } 50 | return node.containsKey("name") && node.get("name") != null; 51 | } 52 | 53 | private OperationDefinition parseOperationDefinition(Map json) { 54 | OperationDefinition.Operation operation = OperationDefinition.Operation.valueOf(((String) json.get("operation")).toUpperCase()); 55 | String name = json.containsKey("name") ? (String) ((Map) json.get("name")).get("value") : null; 56 | 57 | List variableDefinitions = new ArrayList<>(); 58 | List> varDefsJson = (List>) json.get("variableDefinitions"); 59 | if (varDefsJson != null) { 60 | for (Map varDef : varDefsJson) { 61 | variableDefinitions.add(parseVariableDefinition(varDef)); 62 | } 63 | } 64 | 65 | SelectionSet selectionSet = parseSelectionSet((Map) json.get("selectionSet")); 66 | 67 | return OperationDefinition.newOperationDefinition() 68 | .name(name) 69 | .operation(operation) 70 | .variableDefinitions(variableDefinitions) 71 | .selectionSet(selectionSet) 72 | .build(); 73 | } 74 | 75 | private FragmentDefinition parseFragmentDefinition(Map json) { 76 | String name = (String) ((Map) json.get("name")).get("value"); 77 | TypeName typeCondition = TypeName.newTypeName( 78 | (String) ((Map) ((Map) json.get("typeCondition")).get("name")) 79 | .get("value")).build(); 80 | SelectionSet selectionSet = parseSelectionSet((Map) json.get("selectionSet")); 81 | 82 | return FragmentDefinition.newFragmentDefinition() 83 | .name(name) 84 | .typeCondition(typeCondition) 85 | .selectionSet(selectionSet) 86 | .build(); 87 | } 88 | 89 | private VariableDefinition parseVariableDefinition(Map json) { 90 | String name = (String) ((Map) ((Map) json.get("variable")).get("name")) 91 | .get("value"); 92 | Type type = parseType((Map) json.get("type")); 93 | return VariableDefinition.newVariableDefinition() 94 | .name(name) 95 | .type(type) 96 | .build(); 97 | } 98 | 99 | private Type parseType(Map json) { 100 | if ("NonNullType".equals(json.get("kind"))) { 101 | return NonNullType.newNonNullType(parseType((Map) json.get("type"))).build(); 102 | } else if ("ListType".equals(json.get("kind"))) { 103 | return ListType.newListType(parseType((Map) json.get("type"))).build(); 104 | } else { 105 | return TypeName.newTypeName((String) ((Map) json.get("name")).get("value")).build(); 106 | } 107 | } 108 | 109 | private SelectionSet parseSelectionSet(Map json) { 110 | List selections = new ArrayList<>(); 111 | if (json != null) { 112 | List> selectionsJson = (List>) json.get("selections"); 113 | for (Map selectionJson : selectionsJson) { 114 | String kind = (String) selectionJson.get("kind"); 115 | 116 | if ("Field".equals(kind)) { 117 | selections.add(parseField(selectionJson)); 118 | } else if ("FragmentSpread".equals(kind)) { 119 | selections.add(parseFragmentSpread(selectionJson)); 120 | } else if ("InlineFragment".equals(kind)) { // Handling InlineFragment 121 | selections.add(parseInlineFragment(selectionJson)); 122 | } 123 | } 124 | } 125 | return SelectionSet.newSelectionSet().selections(selections).build(); 126 | } 127 | 128 | private Field parseField(Map json) { 129 | String name = (String) ((Map) json.get("name")).get("value"); 130 | 131 | List arguments = new ArrayList<>(); 132 | List> argsJson = (List>) json.get("arguments"); 133 | if (argsJson != null) { 134 | for (Map argJson : argsJson) { 135 | arguments.add(parseArgument(argJson)); 136 | } 137 | } 138 | 139 | SelectionSet selectionSet = parseSelectionSet((Map) json.get("selectionSet")); 140 | 141 | return Field.newField() 142 | .name(name) 143 | .arguments(arguments) 144 | .selectionSet(selectionSet) 145 | .build(); 146 | } 147 | 148 | private Argument parseArgument(Map json) { 149 | String name = (String) ((Map) json.get("name")).get("value"); 150 | Value value = parseValue((Map) json.get("value")); 151 | return Argument.newArgument().name(name).value(value).build(); 152 | } 153 | 154 | private FragmentSpread parseFragmentSpread(Map json) { 155 | String name = (String) ((Map) json.get("name")).get("value"); 156 | return FragmentSpread.newFragmentSpread().name(name).build(); 157 | } 158 | 159 | private InlineFragment parseInlineFragment(Map json) { 160 | TypeName typeCondition = null; 161 | if (json.containsKey("typeCondition")) { 162 | Map namedType = (Map) json.get("typeCondition"); 163 | typeCondition = TypeName.newTypeName((String) ((Map) namedType.get("name")) 164 | .get("value")).build(); 165 | } 166 | SelectionSet selectionSet = parseSelectionSet((Map) json.get("selectionSet")); 167 | 168 | return InlineFragment.newInlineFragment() 169 | .typeCondition(typeCondition) 170 | .selectionSet(selectionSet) 171 | .build(); 172 | } 173 | 174 | private Value parseValue(Map json) { 175 | String kind = (String) json.get("kind"); 176 | 177 | if ("Variable".equals(kind)) { 178 | return VariableReference.newVariableReference() 179 | .name((String) ((Map) json.get("name")).get("value")) 180 | .build(); 181 | } else if ("StringValue".equals(kind)) { 182 | return StringValue.newStringValue((String) json.get("value")).build(); 183 | } else if ("IntValue".equals(kind)) { 184 | return IntValue.newIntValue(new java.math.BigInteger(json.get("value").toString())).build(); 185 | } else if ("BooleanValue".equals(kind)) { 186 | return BooleanValue.newBooleanValue(json.get("value") != null && (Boolean) json.get("value")).build(); 187 | } else if ("NullValue".equals(kind)) { 188 | return NullValue.newNullValue().build(); 189 | } else if ("FloatValue".equals(kind)) { 190 | return FloatValue.newFloatValue(new BigDecimal((String) json.get("value"))).build(); 191 | } else if ("EnumValue".equals(kind)) { 192 | return EnumValue.newEnumValue((String) json.get("value")).build(); 193 | } else if ("ObjectValue".equals(kind)) { 194 | List fields = new ArrayList<>(); 195 | for (Map fieldJson : (List>) json.get("fields")) { 196 | String fieldName = (String) ((Map) fieldJson.get("name")).get("value"); 197 | Value fieldValue = parseValue((Map) fieldJson.get("value")); 198 | fields.add(ObjectField.newObjectField().name(fieldName).value(fieldValue).build()); 199 | } 200 | return ObjectValue.newObjectValue().objectFields(fields).build(); 201 | } 202 | 203 | return NullValue.newNullValue().build(); 204 | } 205 | 206 | } 207 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Apache Maven Wrapper startup batch script, version 3.3.2 23 | # 24 | # Optional ENV vars 25 | # ----------------- 26 | # JAVA_HOME - location of a JDK home dir, required when download maven via java source 27 | # MVNW_REPOURL - repo url base for downloading maven distribution 28 | # MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 29 | # MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output 30 | # ---------------------------------------------------------------------------- 31 | 32 | set -euf 33 | [ "${MVNW_VERBOSE-}" != debug ] || set -x 34 | 35 | # OS specific support. 36 | native_path() { printf %s\\n "$1"; } 37 | case "$(uname)" in 38 | CYGWIN* | MINGW*) 39 | [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" 40 | native_path() { cygpath --path --windows "$1"; } 41 | ;; 42 | esac 43 | 44 | # set JAVACMD and JAVACCMD 45 | set_java_home() { 46 | # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched 47 | if [ -n "${JAVA_HOME-}" ]; then 48 | if [ -x "$JAVA_HOME/jre/sh/java" ]; then 49 | # IBM's JDK on AIX uses strange locations for the executables 50 | JAVACMD="$JAVA_HOME/jre/sh/java" 51 | JAVACCMD="$JAVA_HOME/jre/sh/javac" 52 | else 53 | JAVACMD="$JAVA_HOME/bin/java" 54 | JAVACCMD="$JAVA_HOME/bin/javac" 55 | 56 | if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then 57 | echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 58 | echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 59 | return 1 60 | fi 61 | fi 62 | else 63 | JAVACMD="$( 64 | 'set' +e 65 | 'unset' -f command 2>/dev/null 66 | 'command' -v java 67 | )" || : 68 | JAVACCMD="$( 69 | 'set' +e 70 | 'unset' -f command 2>/dev/null 71 | 'command' -v javac 72 | )" || : 73 | 74 | if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then 75 | echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 76 | return 1 77 | fi 78 | fi 79 | } 80 | 81 | # hash string like Java String::hashCode 82 | hash_string() { 83 | str="${1:-}" h=0 84 | while [ -n "$str" ]; do 85 | char="${str%"${str#?}"}" 86 | h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) 87 | str="${str#?}" 88 | done 89 | printf %x\\n $h 90 | } 91 | 92 | verbose() { :; } 93 | [ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } 94 | 95 | die() { 96 | printf %s\\n "$1" >&2 97 | exit 1 98 | } 99 | 100 | trim() { 101 | # MWRAPPER-139: 102 | # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. 103 | # Needed for removing poorly interpreted newline sequences when running in more 104 | # exotic environments such as mingw bash on Windows. 105 | printf "%s" "${1}" | tr -d '[:space:]' 106 | } 107 | 108 | # parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties 109 | while IFS="=" read -r key value; do 110 | case "${key-}" in 111 | distributionUrl) distributionUrl=$(trim "${value-}") ;; 112 | distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; 113 | esac 114 | done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" 115 | [ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" 116 | 117 | case "${distributionUrl##*/}" in 118 | maven-mvnd-*bin.*) 119 | MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ 120 | case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in 121 | *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; 122 | :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; 123 | :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; 124 | :Linux*x86_64*) distributionPlatform=linux-amd64 ;; 125 | *) 126 | echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 127 | distributionPlatform=linux-amd64 128 | ;; 129 | esac 130 | distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" 131 | ;; 132 | maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; 133 | *) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; 134 | esac 135 | 136 | # apply MVNW_REPOURL and calculate MAVEN_HOME 137 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 138 | [ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" 139 | distributionUrlName="${distributionUrl##*/}" 140 | distributionUrlNameMain="${distributionUrlName%.*}" 141 | distributionUrlNameMain="${distributionUrlNameMain%-bin}" 142 | MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" 143 | MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" 144 | 145 | exec_maven() { 146 | unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : 147 | exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" 148 | } 149 | 150 | if [ -d "$MAVEN_HOME" ]; then 151 | verbose "found existing MAVEN_HOME at $MAVEN_HOME" 152 | exec_maven "$@" 153 | fi 154 | 155 | case "${distributionUrl-}" in 156 | *?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; 157 | *) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; 158 | esac 159 | 160 | # prepare tmp dir 161 | if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then 162 | clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } 163 | trap clean HUP INT TERM EXIT 164 | else 165 | die "cannot create temp dir" 166 | fi 167 | 168 | mkdir -p -- "${MAVEN_HOME%/*}" 169 | 170 | # Download and Install Apache Maven 171 | verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 172 | verbose "Downloading from: $distributionUrl" 173 | verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 174 | 175 | # select .zip or .tar.gz 176 | if ! command -v unzip >/dev/null; then 177 | distributionUrl="${distributionUrl%.zip}.tar.gz" 178 | distributionUrlName="${distributionUrl##*/}" 179 | fi 180 | 181 | # verbose opt 182 | __MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' 183 | [ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v 184 | 185 | # normalize http auth 186 | case "${MVNW_PASSWORD:+has-password}" in 187 | '') MVNW_USERNAME='' MVNW_PASSWORD='' ;; 188 | has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; 189 | esac 190 | 191 | if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then 192 | verbose "Found wget ... using wget" 193 | wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" 194 | elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then 195 | verbose "Found curl ... using curl" 196 | curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" 197 | elif set_java_home; then 198 | verbose "Falling back to use Java to download" 199 | javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" 200 | targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" 201 | cat >"$javaSource" <<-END 202 | public class Downloader extends java.net.Authenticator 203 | { 204 | protected java.net.PasswordAuthentication getPasswordAuthentication() 205 | { 206 | return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); 207 | } 208 | public static void main( String[] args ) throws Exception 209 | { 210 | setDefault( new Downloader() ); 211 | java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); 212 | } 213 | } 214 | END 215 | # For Cygwin/MinGW, switch paths to Windows format before running javac and java 216 | verbose " - Compiling Downloader.java ..." 217 | "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" 218 | verbose " - Running Downloader.java ..." 219 | "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" 220 | fi 221 | 222 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 223 | if [ -n "${distributionSha256Sum-}" ]; then 224 | distributionSha256Result=false 225 | if [ "$MVN_CMD" = mvnd.sh ]; then 226 | echo "Checksum validation is not supported for maven-mvnd." >&2 227 | echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 228 | exit 1 229 | elif command -v sha256sum >/dev/null; then 230 | if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then 231 | distributionSha256Result=true 232 | fi 233 | elif command -v shasum >/dev/null; then 234 | if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then 235 | distributionSha256Result=true 236 | fi 237 | else 238 | echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 239 | echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 240 | exit 1 241 | fi 242 | if [ $distributionSha256Result = false ]; then 243 | echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 244 | echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 245 | exit 1 246 | fi 247 | fi 248 | 249 | # unzip and move 250 | if command -v unzip >/dev/null; then 251 | unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" 252 | else 253 | tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" 254 | fi 255 | printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" 256 | mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" 257 | 258 | clean || : 259 | exec_maven "$@" 260 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # About - GraphQL Extractor 2 | 3 | This is a command line tool which extracts information about GraphQL queries, mutations and subscriptions from schema introspection, remote javascript files and local javascript files. 4 | 5 | The functionality of this tool is to extract the following information: 6 | - GraphQL operations (queries, mutations and subscriptions) from javascript files. 7 | - GraphQL requests in json format from javascript files. 8 | - GraphQL unique operation field names. 9 | - GraphQL field paths from introspection schema. Inspired by lupins 'GraphQL is the new PHP' talk (https://www.youtube.com/watch?v=tIo_t5uUK50&t=696s). 10 | - GraphQL field paths from operations found in javascript files. 11 | 12 | ## Build and installation 13 | 14 | Build is based upon GraalVM native-image. 15 | 16 | ### Prerequisites 17 | 18 | - Oracle GraalVM 17.0.14+8.1 (https://www.oracle.com/java/technologies/downloads/#graalvmjava17) 19 | - GraalVM js language installation 20 | - Microsoft Visual Studio with C++ build tools (https://visualstudio.microsoft.com/visual-cpp-build-tools/) 21 | 22 | #### GraalVM installation 23 | 24 | - Download and extract the GraalVM JDK 17 archive. Into a directory of your choice. 25 | - Install JS language feature for GraalVM by running the following command: 26 | - Set the JAVA_HOME environment variable to the GraalVM JDK 17 directory. 27 | 28 | **GraalVM Setup** 29 | ```shell 30 | sudo tar -xzf graalvm-jdk-17.0.14_linux-x64_bin.tar.gz 31 | cd /usr/lib/jdk/graalvm-jdk-17.0.14+8.1/bin 32 | ./gu install js 33 | ``` 34 | 35 | **Environment vars setup** 36 | ```shell 37 | vim ~/.bashrc 38 | # Add the following JAVA_HOME/PATH setup to bottom of your .bashrc e.g. 39 | #export JAVA_HOME=/usr/lib/jdk/graalvm-jdk-17.0.14+8.1 40 | #export PATH=$JAVA_HOME/bin:$PATH 41 | source ~/.bashrc 42 | ``` 43 | 44 | #### Maven build 45 | 46 | - Clone the repository and navigate to the root directory. 47 | - Build the native image. 48 | 49 | ```shell 50 | git clone https://github.com/pdstat/graphqlextractor.git 51 | cd graphqlextractor 52 | ./mvnw -Pnative native:compile 53 | ``` 54 | 55 | - Copy the built binary executable in the `/target` directory to a directory in your PATH (e.g. `/usr/bin`) and start using the tool :). 56 | 57 | ## Usage 58 | 59 | | Arg | Description | 60 | |--------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 61 | | --help | Outputs usage information | 62 | | --input-directory | The directory containing the javascript files with embedded GQL strings. | 63 | | --input-urls | The path to a wordlist of urls to scan. | 64 | | --input-schema | URL to a graphQL endpoint with introspection enabled or the path to a file containing the json response of an introspection query. | 65 | | --input-operations | The directory containing previously extracted .graphql operations, this avoids resource intensive Javascript AST parsing. | 66 | | --request-header | Request header key/value to set in introspection requests e.g. --request-header="Api-Key1: XXXX" --request-header="Api-Key2: YYYY". | 67 | | --search-field | The field name paths to search for in the schema/operations. | 68 | | --depth | Depth of the field path search, defaults to 10 if not specified. | 69 | | --default-params | The path to a json file of default parameter values. For use with 'requests' output mode | 70 | | --output-directory | The directory where the generated files will be saved. | 71 | | --output-mode | The output mode for the generated files. Possible values are 'requests', 'operations', 'fields', 'paths' and 'all'. The default value is 'requests'. Multiple output modes are supported e.g. --output-mode=requests --output-mode=operations | 72 | 73 | ## Examples 74 | 75 | ### Schema field search 76 | 77 | To search for the possible paths to a field in the schema, use the following command: 78 | 79 | ```shell 80 | gqlextractor --input-schema=https://rickandmortyapi.com/graphql --search-field=name --output-directory=D:\hacking\recon\rickmorty 81 | ``` 82 | 83 | Example screenshot of the output: 84 | 85 | ![Schema field search](images/schema-fields.png) 86 | 87 | To specify the depth of the search, use the `--depth` argument: 88 | 89 | ```shell 90 | gqlextractor --input-schema=https://rickandmortyapi.com/graphql --search-field=name --output-directory=D:\hacking\recon\rickmorty --depth=5 91 | ``` 92 | 93 | Example screenshot of the output: 94 | 95 | ![Schema field search](images/schema-fields-depth.png) 96 | 97 | Paths are also saved to a text file based on the name of the field being searched for. e.g. 98 | 99 | ![Schema field file](images/schema-field-file.png) 100 | 101 | 102 | ### Javascript AST processing 103 | 104 | Don't have a schema? No problem this tool also works by processing the AST of javascript files (both locally and remotely) to extract GraphQL operations. 105 | 106 | Here are some examples of graphql operation formats that are supported 107 | 108 | Template string literals: 109 | ```javascript 110 | const query = gql` 111 | query { 112 | user { 113 | id 114 | name 115 | } 116 | } 117 | `; 118 | ``` 119 | 120 | String literals: 121 | ```javascript 122 | const query = 'query { user { id name } }'; 123 | ``` 124 | 125 | Escaped GraphQL document strings: 126 | ```javascript 127 | const n=JSON.parse("{\"kind\":\"Document\",\"definitions\":[{\"kind\":\"FragmentDefinition\",\"name\":{\"kind\":\"Name\", 128 | \"value\":\"StoccUser\"},\"typeCondition\":{\"kind\":\"NamedType\",\"name\":{\"kind\":\"Name\",\"value\":\"StoccUser\"}}, 129 | \"directives\":[],\"selectionSet\":{\"kind\":\"SelectionSet\",\"selections\":[{\"kind\":\"Field\", 130 | \"name\":{\"kind\":\"Name\",\"value\":\"__typename\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\", 131 | \"name\":{\"kind\":\"Name\",\"value\":\"id\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\", 132 | \"name\":{\"kind\":\"Name\",\"value\":\"email\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\", 133 | \"name\":{\"kind\":\"Name\",\"value\":\"firstName\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\", 134 | \"name\":{\"kind\":\"Name\",\"value\":\"lastName\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\", 135 | \"name\":{\"kind\":\"Name\",\"value\":\"country\"},\"arguments\":[],\"directives\":[]},{\"kind\":\"Field\", 136 | \"name\":{\"kind\":\"Name\",\"value\":\"fullName\"},\"arguments\":[],\"directives\":[]}]}}], 137 | \"definitionId\":\"aaafc494405155bf9a3e5c174a025db9f2db077e9a4931e58ea5d65c7a6da60c\"}") 138 | ``` 139 | 140 | GraphQL documents in javascript objects: 141 | ```javascript 142 | i8={kind:"Document",definitions:[{kind:"OperationDefinition",operation:"mutation",name:{kind:"Name",value:"createInstance"}, 143 | variableDefinitions:[{kind:"VariableDefinition",variable:{kind:"Variable",name:{kind:"Name",value:"input"}}, 144 | type:{kind:"NonNullType",type:{kind:"NamedType",name:{kind:"Name",value:"CreateInstanceInput"}}}}], 145 | selectionSet:{kind:"SelectionSet",selections:[{kind:"Field",name:{kind:"Name",value:"createInstance"}, 146 | arguments:[{kind:"Argument",name:{kind:"Name",value:"input"},value:{kind:"Variable",name:{kind:"Name",value:"input"}}}], 147 | selectionSet:{kind:"SelectionSet",selections:[{kind:"Field",name:{kind:"Name",value:"instance"},selectionSet:{kind:"SelectionSet", 148 | selections:[{kind:"FragmentSpread",name:{kind:"Name",value:"instanceFull"}}]}}]}}]}},{kind:"FragmentDefinition", 149 | name:{kind:"Name",value:"instanceFull"},typeCondition:{kind:"NamedType",name:{kind:"Name",value:"Instance"}}, 150 | selectionSet:{kind:"SelectionSet",selections:[{kind:"Field",name:{kind:"Name",value:"id"}},{kind:"Field", 151 | name:{kind:"Name",value:"name"}},{kind:"Field",name:{kind:"Name",value:"clientId"}},{kind:"Field", 152 | name:{kind:"Name",value:"createdAt"}}]}}]} 153 | ``` 154 | 155 | See below for an example of how these GraphQL documents are successfully reconstructed into a GraphQL operation: 156 | 157 | ```graphql 158 | mutation createInstance($input: CreateInstanceInput!) { 159 | createInstance(input: $input) { 160 | instance { 161 | ...instanceFull 162 | } 163 | } 164 | } 165 | 166 | fragment instanceFull on Instance { 167 | id 168 | name 169 | clientId 170 | createdAt 171 | } 172 | ``` 173 | 174 | #### Extracting operations from javascript files 175 | 176 | This mode will extract GraphQL operations (queries, mutations and subscriptions) from all of the above formats in javascript files. 177 | 178 | ```shell 179 | gqlextractor --input-urls=D:\hacking\recon\caido\input-urls.txt --output-directory=D:\hacking\recon\caido\graphql --output-mode=operations 180 | ``` 181 | 182 | Operations files will be created in an `operations` directory within the output directory. 183 | 184 | ![Operation files](images/operations-list.png) 185 | ![Operation output](images/operation-output.png) 186 | 187 | ### Extracting unique fields from operations 188 | 189 | **NOTE:** The remaining examples will assume operations have already been generated using the previous example. This is to avoid the resource intensive javascript AST parsing. It is however possible to use the `--input-directory` and `--input-urls` arguments to reprocess javascript files. 190 | 191 | This will extract unique fields across all operations. Useful for wordlist generation. (e.g. for fuzzing via clairvoyance) 192 | 193 | ```shell 194 | gqlextractor --input-operations=D:\hacking\recon\caido\graphql\operations --output-directory=D:\hacking\recon\caido\graphql --output-mode=fields 195 | ``` 196 | 197 | A unique fields file will be created in a `wordlist` directory within the output directory. 198 | 199 | ![Unique fields](images/unique-field-output.png) 200 | 201 | ### Extracting json requests from operations 202 | 203 | This will extract the GraphQL requests in json format from the operations. Useful for replaying requests. The collection of requests can be used with Burp via Intruder for example. 204 | 205 | ```shell 206 | gqlextractor --input-operations=D:\hacking\recon\caido\graphql\operations --output-directory=D:\hacking\recon\caido\graphql --output-mode=requests 207 | ``` 208 | 209 | Operations files will be created in an `requests` directory within the output directory. 210 | 211 | ![Requests list](images/requests-list.png) 212 | ![Request output](images/request-output.png) 213 | 214 | #### Using default parameters 215 | 216 | If you have a set of default parameters that you would like to use with the requests, you can specify a json file containing the default parameters. 217 | 218 | This can be useful for example if you have known values of parameters and you're wanting to test IDOR's for example. (Or any other vuln type) 219 | 220 | ```shell 221 | gqlextractor --input-operations=D:\hacking\recon\caido\graphql\operations --default-params=D:\hacking\recon\caido\default-params.json --output-directory=D:\hacking\recon\caido\graphql --output-mode=requests 222 | ``` 223 | 224 | The default parameters file should be in JSON format and contain the parameters you would like to use. For example: 225 | 226 | ```json 227 | { 228 | "input": { 229 | "id": "1", 230 | "name": "TestInstance" 231 | } 232 | } 233 | ``` 234 | 235 | ![Default param output](images/default-param-output.png) 236 | 237 | ### Extracting field paths from operations 238 | 239 | Similar to the schema field search, this will extract the field paths from the operations. This can be useful for understanding the structure of the data returned by the operations. 240 | 241 | ```shell 242 | gqlextractor --input-operations=D:\hacking\recon\caido\graphql\operations --output-directory=D:\hacking\recon\caido\graphql --search-field=email --output-mode=paths 243 | ``` 244 | 245 | The field paths file will be created in a `field-paths` directory within the output directory. 246 | 247 | ![Field paths output](images/field-paths.png) -------------------------------------------------------------------------------- /src/main/resources/walk.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : 3 | typeof define === 'function' && define.amd ? define(['exports'], factory) : 4 | (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory((global.acorn = global.acorn || {}, global.acorn.walk = {}))); 5 | })(this, (function (exports) { 'use strict'; 6 | 7 | // AST walker module for ESTree compatible trees 8 | 9 | // A simple walk is one where you simply specify callbacks to be 10 | // called on specific nodes. The last two arguments are optional. A 11 | // simple use would be 12 | // 13 | // walk.simple(myTree, { 14 | // Expression: function(node) { ... } 15 | // }); 16 | // 17 | // to do something with all expressions. All ESTree node types 18 | // can be used to identify node types, as well as Expression and 19 | // Statement, which denote categories of nodes. 20 | // 21 | // The base argument can be used to pass a custom (recursive) 22 | // walker, and state can be used to give this walked an initial 23 | // state. 24 | 25 | function simple(node, visitors, baseVisitor, state, override) { 26 | if (!baseVisitor) { baseVisitor = base 27 | ; }(function c(node, st, override) { 28 | var type = override || node.type; 29 | baseVisitor[type](node, st, c); 30 | if (visitors[type]) { visitors[type](node, st); } 31 | })(node, state, override); 32 | } 33 | 34 | // An ancestor walk keeps an array of ancestor nodes (including the 35 | // current node) and passes them to the callback as third parameter 36 | // (and also as state parameter when no other state is present). 37 | function ancestor(node, visitors, baseVisitor, state, override) { 38 | var ancestors = []; 39 | if (!baseVisitor) { baseVisitor = base 40 | ; }(function c(node, st, override) { 41 | var type = override || node.type; 42 | var isNew = node !== ancestors[ancestors.length - 1]; 43 | if (isNew) { ancestors.push(node); } 44 | baseVisitor[type](node, st, c); 45 | if (visitors[type]) { visitors[type](node, st || ancestors, ancestors); } 46 | if (isNew) { ancestors.pop(); } 47 | })(node, state, override); 48 | } 49 | 50 | // A recursive walk is one where your functions override the default 51 | // walkers. They can modify and replace the state parameter that's 52 | // threaded through the walk, and can opt how and whether to walk 53 | // their child nodes (by calling their third argument on these 54 | // nodes). 55 | function recursive(node, state, funcs, baseVisitor, override) { 56 | var visitor = funcs ? make(funcs, baseVisitor || undefined) : baseVisitor 57 | ;(function c(node, st, override) { 58 | visitor[override || node.type](node, st, c); 59 | })(node, state, override); 60 | } 61 | 62 | function makeTest(test) { 63 | if (typeof test === "string") 64 | { return function (type) { return type === test; } } 65 | else if (!test) 66 | { return function () { return true; } } 67 | else 68 | { return test } 69 | } 70 | 71 | var Found = function Found(node, state) { this.node = node; this.state = state; }; 72 | 73 | // A full walk triggers the callback on each node 74 | function full(node, callback, baseVisitor, state, override) { 75 | if (!baseVisitor) { baseVisitor = base; } 76 | var last 77 | ;(function c(node, st, override) { 78 | var type = override || node.type; 79 | baseVisitor[type](node, st, c); 80 | if (last !== node) { 81 | callback(node, st, type); 82 | last = node; 83 | } 84 | })(node, state, override); 85 | } 86 | 87 | // An fullAncestor walk is like an ancestor walk, but triggers 88 | // the callback on each node 89 | function fullAncestor(node, callback, baseVisitor, state) { 90 | if (!baseVisitor) { baseVisitor = base; } 91 | var ancestors = [], last 92 | ;(function c(node, st, override) { 93 | var type = override || node.type; 94 | var isNew = node !== ancestors[ancestors.length - 1]; 95 | if (isNew) { ancestors.push(node); } 96 | baseVisitor[type](node, st, c); 97 | if (last !== node) { 98 | callback(node, st || ancestors, ancestors, type); 99 | last = node; 100 | } 101 | if (isNew) { ancestors.pop(); } 102 | })(node, state); 103 | } 104 | 105 | // Find a node with a given start, end, and type (all are optional, 106 | // null can be used as wildcard). Returns a {node, state} object, or 107 | // undefined when it doesn't find a matching node. 108 | function findNodeAt(node, start, end, test, baseVisitor, state) { 109 | if (!baseVisitor) { baseVisitor = base; } 110 | test = makeTest(test); 111 | try { 112 | (function c(node, st, override) { 113 | var type = override || node.type; 114 | if ((start == null || node.start <= start) && 115 | (end == null || node.end >= end)) 116 | { baseVisitor[type](node, st, c); } 117 | if ((start == null || node.start === start) && 118 | (end == null || node.end === end) && 119 | test(type, node)) 120 | { throw new Found(node, st) } 121 | })(node, state); 122 | } catch (e) { 123 | if (e instanceof Found) { return e } 124 | throw e 125 | } 126 | } 127 | 128 | // Find the innermost node of a given type that contains the given 129 | // position. Interface similar to findNodeAt. 130 | function findNodeAround(node, pos, test, baseVisitor, state) { 131 | test = makeTest(test); 132 | if (!baseVisitor) { baseVisitor = base; } 133 | try { 134 | (function c(node, st, override) { 135 | var type = override || node.type; 136 | if (node.start > pos || node.end < pos) { return } 137 | baseVisitor[type](node, st, c); 138 | if (test(type, node)) { throw new Found(node, st) } 139 | })(node, state); 140 | } catch (e) { 141 | if (e instanceof Found) { return e } 142 | throw e 143 | } 144 | } 145 | 146 | // Find the outermost matching node after a given position. 147 | function findNodeAfter(node, pos, test, baseVisitor, state) { 148 | test = makeTest(test); 149 | if (!baseVisitor) { baseVisitor = base; } 150 | try { 151 | (function c(node, st, override) { 152 | if (node.end < pos) { return } 153 | var type = override || node.type; 154 | if (node.start >= pos && test(type, node)) { throw new Found(node, st) } 155 | baseVisitor[type](node, st, c); 156 | })(node, state); 157 | } catch (e) { 158 | if (e instanceof Found) { return e } 159 | throw e 160 | } 161 | } 162 | 163 | // Find the outermost matching node before a given position. 164 | function findNodeBefore(node, pos, test, baseVisitor, state) { 165 | test = makeTest(test); 166 | if (!baseVisitor) { baseVisitor = base; } 167 | var max 168 | ;(function c(node, st, override) { 169 | if (node.start > pos) { return } 170 | var type = override || node.type; 171 | if (node.end <= pos && (!max || max.node.end < node.end) && test(type, node)) 172 | { max = new Found(node, st); } 173 | baseVisitor[type](node, st, c); 174 | })(node, state); 175 | return max 176 | } 177 | 178 | // Used to create a custom walker. Will fill in all missing node 179 | // type properties with the defaults. 180 | function make(funcs, baseVisitor) { 181 | var visitor = Object.create(baseVisitor || base); 182 | for (var type in funcs) { visitor[type] = funcs[type]; } 183 | return visitor 184 | } 185 | 186 | function skipThrough(node, st, c) { c(node, st); } 187 | function ignore(_node, _st, _c) {} 188 | 189 | // Node walkers. 190 | 191 | var base = {}; 192 | 193 | base.Program = base.BlockStatement = base.StaticBlock = function (node, st, c) { 194 | for (var i = 0, list = node.body; i < list.length; i += 1) 195 | { 196 | var stmt = list[i]; 197 | 198 | c(stmt, st, "Statement"); 199 | } 200 | }; 201 | base.Statement = skipThrough; 202 | base.EmptyStatement = ignore; 203 | base.ExpressionStatement = base.ParenthesizedExpression = base.ChainExpression = 204 | function (node, st, c) { return c(node.expression, st, "Expression"); }; 205 | base.IfStatement = function (node, st, c) { 206 | c(node.test, st, "Expression"); 207 | c(node.consequent, st, "Statement"); 208 | if (node.alternate) { c(node.alternate, st, "Statement"); } 209 | }; 210 | base.LabeledStatement = function (node, st, c) { return c(node.body, st, "Statement"); }; 211 | base.BreakStatement = base.ContinueStatement = ignore; 212 | base.WithStatement = function (node, st, c) { 213 | c(node.object, st, "Expression"); 214 | c(node.body, st, "Statement"); 215 | }; 216 | base.SwitchStatement = function (node, st, c) { 217 | c(node.discriminant, st, "Expression"); 218 | for (var i = 0, list = node.cases; i < list.length; i += 1) { 219 | var cs = list[i]; 220 | 221 | c(cs, st); 222 | } 223 | }; 224 | base.SwitchCase = function (node, st, c) { 225 | if (node.test) { c(node.test, st, "Expression"); } 226 | for (var i = 0, list = node.consequent; i < list.length; i += 1) 227 | { 228 | var cons = list[i]; 229 | 230 | c(cons, st, "Statement"); 231 | } 232 | }; 233 | base.ReturnStatement = base.YieldExpression = base.AwaitExpression = function (node, st, c) { 234 | if (node.argument) { c(node.argument, st, "Expression"); } 235 | }; 236 | base.ThrowStatement = base.SpreadElement = 237 | function (node, st, c) { return c(node.argument, st, "Expression"); }; 238 | base.TryStatement = function (node, st, c) { 239 | c(node.block, st, "Statement"); 240 | if (node.handler) { c(node.handler, st); } 241 | if (node.finalizer) { c(node.finalizer, st, "Statement"); } 242 | }; 243 | base.CatchClause = function (node, st, c) { 244 | if (node.param) { c(node.param, st, "Pattern"); } 245 | c(node.body, st, "Statement"); 246 | }; 247 | base.WhileStatement = base.DoWhileStatement = function (node, st, c) { 248 | c(node.test, st, "Expression"); 249 | c(node.body, st, "Statement"); 250 | }; 251 | base.ForStatement = function (node, st, c) { 252 | if (node.init) { c(node.init, st, "ForInit"); } 253 | if (node.test) { c(node.test, st, "Expression"); } 254 | if (node.update) { c(node.update, st, "Expression"); } 255 | c(node.body, st, "Statement"); 256 | }; 257 | base.ForInStatement = base.ForOfStatement = function (node, st, c) { 258 | c(node.left, st, "ForInit"); 259 | c(node.right, st, "Expression"); 260 | c(node.body, st, "Statement"); 261 | }; 262 | base.ForInit = function (node, st, c) { 263 | if (node.type === "VariableDeclaration") { c(node, st); } 264 | else { c(node, st, "Expression"); } 265 | }; 266 | base.DebuggerStatement = ignore; 267 | 268 | base.FunctionDeclaration = function (node, st, c) { return c(node, st, "Function"); }; 269 | base.VariableDeclaration = function (node, st, c) { 270 | for (var i = 0, list = node.declarations; i < list.length; i += 1) 271 | { 272 | var decl = list[i]; 273 | 274 | c(decl, st); 275 | } 276 | }; 277 | base.VariableDeclarator = function (node, st, c) { 278 | c(node.id, st, "Pattern"); 279 | if (node.init) { c(node.init, st, "Expression"); } 280 | }; 281 | 282 | base.Function = function (node, st, c) { 283 | if (node.id) { c(node.id, st, "Pattern"); } 284 | for (var i = 0, list = node.params; i < list.length; i += 1) 285 | { 286 | var param = list[i]; 287 | 288 | c(param, st, "Pattern"); 289 | } 290 | c(node.body, st, node.expression ? "Expression" : "Statement"); 291 | }; 292 | 293 | base.Pattern = function (node, st, c) { 294 | if (node.type === "Identifier") 295 | { c(node, st, "VariablePattern"); } 296 | else if (node.type === "MemberExpression") 297 | { c(node, st, "MemberPattern"); } 298 | else 299 | { c(node, st); } 300 | }; 301 | base.VariablePattern = ignore; 302 | base.MemberPattern = skipThrough; 303 | base.RestElement = function (node, st, c) { return c(node.argument, st, "Pattern"); }; 304 | base.ArrayPattern = function (node, st, c) { 305 | for (var i = 0, list = node.elements; i < list.length; i += 1) { 306 | var elt = list[i]; 307 | 308 | if (elt) { c(elt, st, "Pattern"); } 309 | } 310 | }; 311 | base.ObjectPattern = function (node, st, c) { 312 | for (var i = 0, list = node.properties; i < list.length; i += 1) { 313 | var prop = list[i]; 314 | 315 | if (prop.type === "Property") { 316 | if (prop.computed) { c(prop.key, st, "Expression"); } 317 | c(prop.value, st, "Pattern"); 318 | } else if (prop.type === "RestElement") { 319 | c(prop.argument, st, "Pattern"); 320 | } 321 | } 322 | }; 323 | 324 | base.Expression = skipThrough; 325 | base.ThisExpression = base.Super = base.MetaProperty = ignore; 326 | base.ArrayExpression = function (node, st, c) { 327 | for (var i = 0, list = node.elements; i < list.length; i += 1) { 328 | var elt = list[i]; 329 | 330 | if (elt) { c(elt, st, "Expression"); } 331 | } 332 | }; 333 | base.ObjectExpression = function (node, st, c) { 334 | for (var i = 0, list = node.properties; i < list.length; i += 1) 335 | { 336 | var prop = list[i]; 337 | 338 | c(prop, st); 339 | } 340 | }; 341 | base.FunctionExpression = base.ArrowFunctionExpression = base.FunctionDeclaration; 342 | base.SequenceExpression = function (node, st, c) { 343 | for (var i = 0, list = node.expressions; i < list.length; i += 1) 344 | { 345 | var expr = list[i]; 346 | 347 | c(expr, st, "Expression"); 348 | } 349 | }; 350 | base.TemplateLiteral = function (node, st, c) { 351 | for (var i = 0, list = node.quasis; i < list.length; i += 1) 352 | { 353 | var quasi = list[i]; 354 | 355 | c(quasi, st); 356 | } 357 | 358 | for (var i$1 = 0, list$1 = node.expressions; i$1 < list$1.length; i$1 += 1) 359 | { 360 | var expr = list$1[i$1]; 361 | 362 | c(expr, st, "Expression"); 363 | } 364 | }; 365 | base.TemplateElement = ignore; 366 | base.UnaryExpression = base.UpdateExpression = function (node, st, c) { 367 | c(node.argument, st, "Expression"); 368 | }; 369 | base.BinaryExpression = base.LogicalExpression = function (node, st, c) { 370 | c(node.left, st, "Expression"); 371 | c(node.right, st, "Expression"); 372 | }; 373 | base.AssignmentExpression = base.AssignmentPattern = function (node, st, c) { 374 | c(node.left, st, "Pattern"); 375 | c(node.right, st, "Expression"); 376 | }; 377 | base.ConditionalExpression = function (node, st, c) { 378 | c(node.test, st, "Expression"); 379 | c(node.consequent, st, "Expression"); 380 | c(node.alternate, st, "Expression"); 381 | }; 382 | base.NewExpression = base.CallExpression = function (node, st, c) { 383 | c(node.callee, st, "Expression"); 384 | if (node.arguments) 385 | { for (var i = 0, list = node.arguments; i < list.length; i += 1) 386 | { 387 | var arg = list[i]; 388 | 389 | c(arg, st, "Expression"); 390 | } } 391 | }; 392 | base.MemberExpression = function (node, st, c) { 393 | c(node.object, st, "Expression"); 394 | if (node.computed) { c(node.property, st, "Expression"); } 395 | }; 396 | base.ExportNamedDeclaration = base.ExportDefaultDeclaration = function (node, st, c) { 397 | if (node.declaration) 398 | { c(node.declaration, st, node.type === "ExportNamedDeclaration" || node.declaration.id ? "Statement" : "Expression"); } 399 | if (node.source) { c(node.source, st, "Expression"); } 400 | }; 401 | base.ExportAllDeclaration = function (node, st, c) { 402 | if (node.exported) 403 | { c(node.exported, st); } 404 | c(node.source, st, "Expression"); 405 | }; 406 | base.ImportDeclaration = function (node, st, c) { 407 | for (var i = 0, list = node.specifiers; i < list.length; i += 1) 408 | { 409 | var spec = list[i]; 410 | 411 | c(spec, st); 412 | } 413 | c(node.source, st, "Expression"); 414 | }; 415 | base.ImportExpression = function (node, st, c) { 416 | c(node.source, st, "Expression"); 417 | }; 418 | base.ImportSpecifier = base.ImportDefaultSpecifier = base.ImportNamespaceSpecifier = base.Identifier = base.PrivateIdentifier = base.Literal = ignore; 419 | 420 | base.TaggedTemplateExpression = function (node, st, c) { 421 | c(node.tag, st, "Expression"); 422 | c(node.quasi, st, "Expression"); 423 | }; 424 | base.ClassDeclaration = base.ClassExpression = function (node, st, c) { return c(node, st, "Class"); }; 425 | base.Class = function (node, st, c) { 426 | if (node.id) { c(node.id, st, "Pattern"); } 427 | if (node.superClass) { c(node.superClass, st, "Expression"); } 428 | c(node.body, st); 429 | }; 430 | base.ClassBody = function (node, st, c) { 431 | for (var i = 0, list = node.body; i < list.length; i += 1) 432 | { 433 | var elt = list[i]; 434 | 435 | c(elt, st); 436 | } 437 | }; 438 | base.MethodDefinition = base.PropertyDefinition = base.Property = function (node, st, c) { 439 | if (node.computed) { c(node.key, st, "Expression"); } 440 | if (node.value) { c(node.value, st, "Expression"); } 441 | }; 442 | 443 | exports.ancestor = ancestor; 444 | exports.base = base; 445 | exports.findNodeAfter = findNodeAfter; 446 | exports.findNodeAround = findNodeAround; 447 | exports.findNodeAt = findNodeAt; 448 | exports.findNodeBefore = findNodeBefore; 449 | exports.full = full; 450 | exports.fullAncestor = fullAncestor; 451 | exports.make = make; 452 | exports.recursive = recursive; 453 | exports.simple = simple; 454 | 455 | })); 456 | --------------------------------------------------------------------------------