diff --git a/src/main/java/graphql/introspection/Introspection.java b/src/main/java/graphql/introspection/Introspection.java index 9db9414bc8..15c41f3a77 100644 --- a/src/main/java/graphql/introspection/Introspection.java +++ b/src/main/java/graphql/introspection/Introspection.java @@ -90,7 +90,7 @@ public enum TypeKind { public static final GraphQLEnumType __TypeKind = GraphQLEnumType.newEnum() .name("__TypeKind") .description("An enum describing what kind of type a given __Type is") - .value("SCALAR", TypeKind.SCALAR, "Indicates this type is a scalar. 'specifiedByUrl' is a valid field") + .value("SCALAR", TypeKind.SCALAR, "Indicates this type is a scalar. 'specifiedByURL' is a valid field") .value("OBJECT", TypeKind.OBJECT, "Indicates this type is an object. `fields` and `interfaces` are valid fields.") .value("INTERFACE", TypeKind.INTERFACE, "Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.") .value("UNION", TypeKind.UNION, "Indicates this type is a union. `possibleTypes` is a valid field.") @@ -398,8 +398,13 @@ private static String printDefaultValue(InputValueWithState inputValueWithState, .name("ofType") .type(typeRef("__Type"))) .field(newFieldDefinition() - .name("specifiedByUrl") + .name("specifiedByURL") .type(GraphQLString)) + .field(newFieldDefinition() + .name("specifiedByUrl") + .type(GraphQLString) + .deprecate("see `specifiedByURL`") + ) .build(); static { @@ -412,7 +417,8 @@ private static String printDefaultValue(InputValueWithState inputValueWithState, register(__Type, "ofType", OfTypeFetcher); register(__Type, "name", nameDataFetcher); register(__Type, "description", descriptionDataFetcher); - register(__Type, "specifiedByUrl", specifiedByUrlDataFetcher); + register(__Type, "specifiedByURL", specifiedByUrlDataFetcher); + register(__Type, "specifiedByUrl", specifiedByUrlDataFetcher); // note that this field is deprecated } diff --git a/src/main/java/graphql/introspection/IntrospectionQuery.java b/src/main/java/graphql/introspection/IntrospectionQuery.java index f93617951f..4d372fe992 100644 --- a/src/main/java/graphql/introspection/IntrospectionQuery.java +++ b/src/main/java/graphql/introspection/IntrospectionQuery.java @@ -4,106 +4,5 @@ @PublicApi public interface IntrospectionQuery { - - String INTROSPECTION_QUERY = "\n" + - " query IntrospectionQuery {\n" + - " __schema {\n" + - " queryType { name }\n" + - " mutationType { name }\n" + - " subscriptionType { name }\n" + - " types {\n" + - " ...FullType\n" + - " }\n" + - " directives {\n" + - " name\n" + - " description\n" + - " locations\n" + - " args(includeDeprecated: true) {\n" + - " ...InputValue\n" + - " }\n" + - " isRepeatable\n" + - " }\n" + - " }\n" + - " }\n" + - "\n" + - " fragment FullType on __Type {\n" + - " kind\n" + - " name\n" + - " description\n" + - " fields(includeDeprecated: true) {\n" + - " name\n" + - " description\n" + - " args(includeDeprecated: true) {\n" + - " ...InputValue\n" + - " }\n" + - " type {\n" + - " ...TypeRef\n" + - " }\n" + - " isDeprecated\n" + - " deprecationReason\n" + - " }\n" + - " inputFields(includeDeprecated: true) {\n" + - " ...InputValue\n" + - " }\n" + - " interfaces {\n" + - " ...TypeRef\n" + - " }\n" + - " enumValues(includeDeprecated: true) {\n" + - " name\n" + - " description\n" + - " isDeprecated\n" + - " deprecationReason\n" + - " }\n" + - " possibleTypes {\n" + - " ...TypeRef\n" + - " }\n" + - " }\n" + - "\n" + - " fragment InputValue on __InputValue {\n" + - " name\n" + - " description\n" + - " type { ...TypeRef }\n" + - " defaultValue\n" + - " isDeprecated\n" + - " deprecationReason\n" + - " }\n" + - "\n" + - // - // The depth of the types is actually an arbitrary decision. It could be any depth in fact. This depth - // was taken from GraphIQL https://github.com/graphql/graphiql/blob/master/src/utility/introspectionQueries.js - // which uses 7 levels and hence could represent a type like say [[[[[Float!]]]]] - // - "fragment TypeRef on __Type {\n" + - " kind\n" + - " name\n" + - " ofType {\n" + - " kind\n" + - " name\n" + - " ofType {\n" + - " kind\n" + - " name\n" + - " ofType {\n" + - " kind\n" + - " name\n" + - " ofType {\n" + - " kind\n" + - " name\n" + - " ofType {\n" + - " kind\n" + - " name\n" + - " ofType {\n" + - " kind\n" + - " name\n" + - " ofType {\n" + - " kind\n" + - " name\n" + - " }\n" + - " }\n" + - " }\n" + - " }\n" + - " }\n" + - " }\n" + - " }\n" + - " }\n" + - "\n"; + String INTROSPECTION_QUERY = IntrospectionQueryBuilder.build(); } diff --git a/src/main/java/graphql/introspection/IntrospectionQueryBuilder.java b/src/main/java/graphql/introspection/IntrospectionQueryBuilder.java new file mode 100644 index 0000000000..cece9aeec1 --- /dev/null +++ b/src/main/java/graphql/introspection/IntrospectionQueryBuilder.java @@ -0,0 +1,356 @@ +package graphql.introspection; + +import com.google.common.collect.ImmutableList; +import graphql.language.Argument; +import graphql.language.AstPrinter; +import graphql.language.BooleanValue; +import graphql.language.Document; +import graphql.language.Field; +import graphql.language.FragmentDefinition; +import graphql.language.FragmentSpread; +import graphql.language.OperationDefinition; +import graphql.language.SelectionSet; +import graphql.language.TypeName; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +public class IntrospectionQueryBuilder { + public static class Options { + + private final boolean descriptions; + + private final boolean specifiedByUrl; + + private final boolean directiveIsRepeatable; + + private final boolean schemaDescription; + + private final boolean inputValueDeprecation; + + private final int typeRefFragmentDepth; + + private Options(boolean descriptions, + boolean specifiedByUrl, + boolean directiveIsRepeatable, + boolean schemaDescription, + boolean inputValueDeprecation, + int typeRefFragmentDepth) { + this.descriptions = descriptions; + this.specifiedByUrl = specifiedByUrl; + this.directiveIsRepeatable = directiveIsRepeatable; + this.schemaDescription = schemaDescription; + this.inputValueDeprecation = inputValueDeprecation; + this.typeRefFragmentDepth = typeRefFragmentDepth; + } + + public boolean isDescriptions() { + return descriptions; + } + + public boolean isSpecifiedByUrl() { + return specifiedByUrl; + } + + public boolean isDirectiveIsRepeatable() { + return directiveIsRepeatable; + } + + public boolean isSchemaDescription() { + return schemaDescription; + } + + public boolean isInputValueDeprecation() { + return inputValueDeprecation; + } + + public int getTypeRefFragmentDepth() { + return typeRefFragmentDepth; + } + + public static Options defaultOptions() { + return new Options( + true, + false, + true, + false, + true, + 7 + ); + } + + /** + * This will allow you to include description fields in the introspection query + * + * @param flag whether to include them + * + * @return options + */ + public Options descriptions(boolean flag) { + return new Options(flag, + this.specifiedByUrl, + this.directiveIsRepeatable, + this.schemaDescription, + this.inputValueDeprecation, + this.typeRefFragmentDepth); + } + + /** + * This will allow you to include the `specifiedByURL` field for scalar types in the introspection query. + * + * @param flag whether to include them + * + * @return options + */ + public Options specifiedByUrl(boolean flag) { + return new Options(this.descriptions, + flag, + this.directiveIsRepeatable, + this.schemaDescription, + this.inputValueDeprecation, + this.typeRefFragmentDepth); + } + + /** + * This will allow you to include the `isRepeatable` field for directives in the introspection query. + * + * @param flag whether to include them + * + * @return options + */ + public Options directiveIsRepeatable(boolean flag) { + return new Options(this.descriptions, + this.specifiedByUrl, + flag, + this.schemaDescription, + this.inputValueDeprecation, + this.typeRefFragmentDepth); + } + + /** + * This will allow you to include the `description` field for the schema type in the introspection query. + * + * @param flag whether to include them + * + * @return options + */ + public Options schemaDescription(boolean flag) { + return new Options(this.descriptions, + this.specifiedByUrl, + this.directiveIsRepeatable, + flag, + this.inputValueDeprecation, + this.typeRefFragmentDepth); + } + + /** + * This will allow you to include deprecated input fields in the introspection query. + * + * @param flag whether to include them + * + * @return options + */ + public Options inputValueDeprecation(boolean flag) { + return new Options(this.descriptions, + this.specifiedByUrl, + this.directiveIsRepeatable, + this.schemaDescription, + flag, + this.typeRefFragmentDepth); + } + + /** + * This will allow you to control the depth of the `TypeRef` fragment in the introspection query. + * + * @param typeRefFragmentDepth the depth of the `TypeRef` fragment. + * + * @return options + */ + public Options typeRefFragmentDepth(int typeRefFragmentDepth) { + return new Options(this.descriptions, + this.specifiedByUrl, + this.directiveIsRepeatable, + this.schemaDescription, + this.inputValueDeprecation, + typeRefFragmentDepth); + } + } + + private static List filter(T... args) { + return Arrays.stream(args).filter(Objects::nonNull).collect(Collectors.toList()); + } + + public static String build() { + return build(Options.defaultOptions()); + } + + public static String build(Options options) { + SelectionSet schemaSelectionSet = SelectionSet.newSelectionSet().selections( filter( + options.schemaDescription ? Field.newField("description").build() : null, + Field.newField("queryType", SelectionSet.newSelectionSet() + .selection( Field.newField("name").build() ) + .build() + ) + .build(), + Field.newField("mutationType", SelectionSet.newSelectionSet() + .selection( Field.newField("name").build() ) + .build() + ) + .build(), + Field.newField("subscriptionType", SelectionSet.newSelectionSet() + .selection( Field.newField("name").build() ) + .build() + ) + .build(), + Field.newField("types", SelectionSet.newSelectionSet() + .selection( FragmentSpread.newFragmentSpread("FullType").build() ) + .build() + ) + .build(), + Field.newField("directives", SelectionSet.newSelectionSet().selections( filter( + Field.newField("name").build(), + options.descriptions ? Field.newField("description").build() : null, + Field.newField("locations").build(), + Field.newField("args") + .arguments( filter( + options.inputValueDeprecation ? Argument.newArgument("includeDeprecated", BooleanValue.of(true)).build() : null + ) ) + .selectionSet( SelectionSet.newSelectionSet() + .selection( FragmentSpread.newFragmentSpread("InputValue").build() ) + .build() + ) + .build(), + options.directiveIsRepeatable ? Field.newField("isRepeatable").build() : null + ) ) + .build() + ) + .build() + ) + ).build(); + + SelectionSet fullTypeSelectionSet = SelectionSet.newSelectionSet().selections( filter( + Field.newField("kind").build(), + Field.newField("name").build(), + options.descriptions ? Field.newField("description").build() : null, + options.specifiedByUrl ? Field.newField("specifiedByURL").build() : null, + Field.newField("fields") + .arguments( ImmutableList.of( + Argument.newArgument("includeDeprecated", BooleanValue.of(true)).build() + ) ) + .selectionSet( SelectionSet.newSelectionSet().selections( filter( + Field.newField("name").build(), + options.descriptions ? Field.newField("description").build() : null, + Field.newField("args") + .arguments( filter( + options.inputValueDeprecation ? Argument.newArgument("includeDeprecated", BooleanValue.of(true)).build() : null + ) ) + .selectionSet( SelectionSet.newSelectionSet() + .selection( FragmentSpread.newFragmentSpread("InputValue").build() ) + .build() + ) + .build(), + Field.newField("type", SelectionSet.newSelectionSet() + .selection( FragmentSpread.newFragmentSpread("TypeRef").build() ) + .build() + ) + .build(), + Field.newField("isDeprecated").build(), + Field.newField("deprecationReason").build() + ) ).build() + ) + .build(), + Field.newField("inputFields") + .arguments( filter( + options.inputValueDeprecation ? Argument.newArgument("includeDeprecated", BooleanValue.of(true)).build() : null + ) ) + .selectionSet( SelectionSet.newSelectionSet() + .selection( FragmentSpread.newFragmentSpread("InputValue").build() ) + .build() + ) + .build(), + Field.newField("interfaces", SelectionSet.newSelectionSet() + .selection( FragmentSpread.newFragmentSpread("TypeRef").build() ) + .build() + ) + .build(), + Field.newField("enumValues") + .arguments( ImmutableList.of( + Argument.newArgument("includeDeprecated", BooleanValue.of(true)).build() + ) ) + .selectionSet( SelectionSet.newSelectionSet().selections( filter( + Field.newField("name").build(), + options.descriptions ? Field.newField("description").build() : null, + Field.newField("isDeprecated").build(), + Field.newField("deprecationReason").build() + ) ) + .build() + ) + .build(), + Field.newField("possibleTypes", SelectionSet.newSelectionSet() + .selection( FragmentSpread.newFragmentSpread("TypeRef").build() ) + .build() + ) + .build() + ) ).build(); + + SelectionSet inputValueSelectionSet = SelectionSet.newSelectionSet().selections( filter( + Field.newField("name").build(), + options.descriptions ? Field.newField("description").build() : null, + Field.newField("type", SelectionSet.newSelectionSet() + .selection( FragmentSpread.newFragmentSpread("TypeRef").build() ) + .build() + ) + .build(), + Field.newField("defaultValue").build(), + options.inputValueDeprecation ? Field.newField("isDeprecated").build() : null, + options.inputValueDeprecation ? Field.newField("deprecationReason").build() : null + ) ).build(); + + SelectionSet typeRefSelectionSet = SelectionSet.newSelectionSet().selections( filter( + Field.newField("kind").build(), + Field.newField("name").build() + ) ).build(); + + for(int i=options.typeRefFragmentDepth; i>0; i-=1) { + typeRefSelectionSet = SelectionSet.newSelectionSet().selections( filter( + Field.newField("kind").build(), + Field.newField("name").build(), + Field.newField("ofType", typeRefSelectionSet).build() + ) ).build(); + } + + Document query = Document.newDocument() + .definition( OperationDefinition.newOperationDefinition() + .operation(OperationDefinition.Operation.QUERY) + .name("IntrospectionQuery") + .selectionSet( SelectionSet.newSelectionSet() + .selection( Field.newField("__schema", schemaSelectionSet).build() ) + .build() + ) + .build() + ) + .definition( FragmentDefinition.newFragmentDefinition() + .name("FullType") + .typeCondition( TypeName.newTypeName().name("__Type").build() ) + .selectionSet(fullTypeSelectionSet) + .build() + ) + .definition( FragmentDefinition.newFragmentDefinition() + .name("InputValue") + .typeCondition( TypeName.newTypeName().name("__InputValue").build() ) + .selectionSet(inputValueSelectionSet) + .build() + ) + .definition( FragmentDefinition.newFragmentDefinition() + .name("TypeRef") + .typeCondition( TypeName.newTypeName().name("__Type").build() ) + .selectionSet(typeRefSelectionSet) + .build() + ) + .build(); + + return AstPrinter.printAst(query); + } +} diff --git a/src/test/groovy/graphql/GraphQLTest.groovy b/src/test/groovy/graphql/GraphQLTest.groovy index 505350e74b..ef2d4e19f2 100644 --- a/src/test/groovy/graphql/GraphQLTest.groovy +++ b/src/test/groovy/graphql/GraphQLTest.groovy @@ -1251,10 +1251,10 @@ many lines'''] GraphQLSchema schema = TestUtil.schema('type Query {foo: MyScalar} scalar MyScalar @specifiedBy(url:"myUrl")') when: - def result = GraphQL.newGraphQL(schema).build().execute('{__type(name: "MyScalar") {name specifiedByUrl}}').getData() + def result = GraphQL.newGraphQL(schema).build().execute('{__type(name: "MyScalar") {name specifiedByURL}}').getData() then: - result == [__type: [name: "MyScalar", specifiedByUrl: "myUrl"]] + result == [__type: [name: "MyScalar", specifiedByURL: "myUrl"]] } def "test DFR and CF"() { diff --git a/src/test/groovy/graphql/introspection/IntrospectionTest.groovy b/src/test/groovy/graphql/introspection/IntrospectionTest.groovy index 18f259cbed..712ac8048c 100644 --- a/src/test/groovy/graphql/introspection/IntrospectionTest.groovy +++ b/src/test/groovy/graphql/introspection/IntrospectionTest.groovy @@ -435,4 +435,191 @@ class IntrospectionTest extends Specification { graphql.execute(query).data == [__type: [fields: [[args: [[defaultValue: '{inputField : "foo"}']]]]]] } + + def "test AST printed introspection query is equivalent to original string"() { + when: + def oldIntrospectionQuery = "\n" + + " query IntrospectionQuery {\n" + + " __schema {\n" + + " queryType { name }\n" + + " mutationType { name }\n" + + " subscriptionType { name }\n" + + " types {\n" + + " ...FullType\n" + + " }\n" + + " directives {\n" + + " name\n" + + " description\n" + + " locations\n" + + " args(includeDeprecated: true) {\n" + + " ...InputValue\n" + + " }\n" + + " isRepeatable\n" + + " }\n" + + " }\n" + + " }\n" + + "\n" + + " fragment FullType on __Type {\n" + + " kind\n" + + " name\n" + + " description\n" + + " fields(includeDeprecated: true) {\n" + + " name\n" + + " description\n" + + " args(includeDeprecated: true) {\n" + + " ...InputValue\n" + + " }\n" + + " type {\n" + + " ...TypeRef\n" + + " }\n" + + " isDeprecated\n" + + " deprecationReason\n" + + " }\n" + + " inputFields(includeDeprecated: true) {\n" + + " ...InputValue\n" + + " }\n" + + " interfaces {\n" + + " ...TypeRef\n" + + " }\n" + + " enumValues(includeDeprecated: true) {\n" + + " name\n" + + " description\n" + + " isDeprecated\n" + + " deprecationReason\n" + + " }\n" + + " possibleTypes {\n" + + " ...TypeRef\n" + + " }\n" + + " }\n" + + "\n" + + " fragment InputValue on __InputValue {\n" + + " name\n" + + " description\n" + + " type { ...TypeRef }\n" + + " defaultValue\n" + + " isDeprecated\n" + + " deprecationReason\n" + + " }\n" + + "\n" + + // + // The depth of the types is actually an arbitrary decision. It could be any depth in fact. This depth + // was taken from GraphIQL https://github.com/graphql/graphiql/blob/master/src/utility/introspectionQueries.js + // which uses 7 levels and hence could represent a type like say [[[[[Float!]]]]] + // + "fragment TypeRef on __Type {\n" + + " kind\n" + + " name\n" + + " ofType {\n" + + " kind\n" + + " name\n" + + " ofType {\n" + + " kind\n" + + " name\n" + + " ofType {\n" + + " kind\n" + + " name\n" + + " ofType {\n" + + " kind\n" + + " name\n" + + " ofType {\n" + + " kind\n" + + " name\n" + + " ofType {\n" + + " kind\n" + + " name\n" + + " ofType {\n" + + " kind\n" + + " name\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + "\n" + + def newIntrospectionQuery = IntrospectionQuery.INTROSPECTION_QUERY; + + then: + oldIntrospectionQuery.replaceAll("\\s+","").equals( + newIntrospectionQuery.replaceAll("\\s+","") + ) + } + + def "test parameterized introspection queries"() { + def spec = ''' + scalar UUID @specifiedBy(url: "https://tools.ietf.org/html/rfc4122") + + directive @repeatableDirective(arg: String) repeatable on FIELD + + """schema description""" + schema { + query: Query + } + + directive @someDirective( + deprecatedArg : String @deprecated + notDeprecatedArg : String + ) repeatable on FIELD + + type Query { + """notDeprecated root field description""" + notDeprecated(arg : InputType @deprecated, notDeprecatedArg : InputType) : Enum + tenDimensionalList : [[[[[[[[[[String]]]]]]]]]] + } + enum Enum { + RED @deprecated + BLUE + } + input InputType { + inputField : String @deprecated + } + ''' + + def graphQL = TestUtil.graphQL(spec).build() + + def parseExecutionResult = { + [ + it.data["__schema"]["types"].find{it["name"] == "Query"}["fields"].find{it["name"] == "notDeprecated"}["description"] != null, // descriptions is true + it.data["__schema"]["types"].find{it["name"] == "UUID"}["specifiedByURL"] != null, // specifiedByUrl is true + it.data["__schema"]["directives"].find{it["name"] == "repeatableDirective"}["isRepeatable"] != null, // directiveIsRepeatable is true + it.data["__schema"]["description"] != null, // schemaDescription is true + it.data["__schema"]["types"].find { it['name'] == 'InputType' }["inputFields"].find({ it["name"] == "inputField" }) != null // inputValueDeprecation is true + ] + } + + when: + def allFalseExecutionResult = graphQL.execute( + IntrospectionQueryBuilder.build( + IntrospectionQueryBuilder.Options.defaultOptions() + .descriptions(false) + .specifiedByUrl(false) + .directiveIsRepeatable(false) + .schemaDescription(false) + .inputValueDeprecation(false) + .typeRefFragmentDepth(5) + ) + ) + then: + !parseExecutionResult(allFalseExecutionResult).any() + allFalseExecutionResult.data["__schema"]["types"].find{it["name"] == "Query"}["fields"].find{it["name"] == "tenDimensionalList"}["type"]["ofType"]["ofType"]["ofType"]["ofType"]["ofType"]["ofType"] == null // typeRefFragmentDepth is 5 + + when: + def allTrueExecutionResult = graphQL.execute( + IntrospectionQueryBuilder.build( + IntrospectionQueryBuilder.Options.defaultOptions() + .descriptions(true) + .specifiedByUrl(true) + .directiveIsRepeatable(true) + .schemaDescription(true) + .inputValueDeprecation(true) + .typeRefFragmentDepth(7) + ) + ) + then: + parseExecutionResult(allTrueExecutionResult).every() + allTrueExecutionResult.data["__schema"]["types"].find{it["name"] == "Query"}["fields"].find{it["name"] == "tenDimensionalList"}["type"]["ofType"]["ofType"]["ofType"]["ofType"]["ofType"]["ofType"]["ofType"]["ofType"] == null // typeRefFragmentDepth is 7 + } }