diff --git a/pom.xml b/pom.xml index 834a9f849..ba7786304 100644 --- a/pom.xml +++ b/pom.xml @@ -29,13 +29,13 @@ 2.0.0-RC1 1.1.2 0.33.0 + 2.0.4 3.0.2 2.1.1 5.0.0 2.0.0 - - 18.1 + 19.0 1.9.3 4.3.3 diff --git a/server/implementation-cdi/src/main/java/io/smallrye/graphql/cdi/config/ConfigKey.java b/server/implementation-cdi/src/main/java/io/smallrye/graphql/cdi/config/ConfigKey.java index a358ed73f..b97c93d13 100644 --- a/server/implementation-cdi/src/main/java/io/smallrye/graphql/cdi/config/ConfigKey.java +++ b/server/implementation-cdi/src/main/java/io/smallrye/graphql/cdi/config/ConfigKey.java @@ -22,5 +22,12 @@ public interface ConfigKey extends org.eclipse.microprofile.graphql.ConfigKey { public static final String ERROR_EXTENSION_FIELDS = "smallrye.graphql.errorExtensionFields"; public static final String FIELD_VISIBILITY = "smallrye.graphql.fieldVisibility"; public static final String UNWRAP_EXCEPTIONS = "smallrye.graphql.unwrapExceptions"; + public static final String PARSER_CAPTURE_IGNORED_CHARS = "smallrye.graphql.parser.capture.ignoredChars"; + public static final String PARSER_CAPTURE_LINE_COMMENTS = "smallrye.graphql.parser.capture.lineComments"; + public static final String PARSER_CAPTURE_SOURCE_LOCATION = "smallrye.graphql.parser.capture.sourceLocation"; + public static final String PARSER_MAX_TOKENS = "smallrye.graphql.parser.maxTokens"; + public static final String PARSER_MAX_WHITESPACE_TOKENS = "smallrye.graphql.parser.maxWhitespaceTokens"; + public static final String INSTRUMENTATION_QUERY_COMPLEXITY = "smallrye.graphql.instrumentation.queryComplexity"; + public static final String INSTRUMENTATION_QUERY_DEPTH = "smallrye.graphql.instrumentation.queryDepth"; -} +} \ No newline at end of file diff --git a/server/implementation-cdi/src/main/java/io/smallrye/graphql/cdi/config/MicroProfileConfig.java b/server/implementation-cdi/src/main/java/io/smallrye/graphql/cdi/config/MicroProfileConfig.java index db4ce3837..ea1623d25 100644 --- a/server/implementation-cdi/src/main/java/io/smallrye/graphql/cdi/config/MicroProfileConfig.java +++ b/server/implementation-cdi/src/main/java/io/smallrye/graphql/cdi/config/MicroProfileConfig.java @@ -34,6 +34,13 @@ public class MicroProfileConfig implements Config { private String fieldVisibility; private Optional> unwrapExceptions; private Optional> errorExtensionFields; + private Optional parserMaxTokens; + private Optional parserMaxWhitespaceTokens; + private Optional parserCaptureSourceLocation; + private Optional parserCaptureLineComments; + private Optional parserCaptureIgnoredChars; + private Optional queryComplexityInstrumentation; + private Optional queryDepthInstrumentation; @Override public String getName() { @@ -168,6 +175,55 @@ public LogPayloadOption logPayload() { return logPayload; } + @Override + public Optional isParserCaptureIgnoredChars() { + if (parserCaptureIgnoredChars == null) { + org.eclipse.microprofile.config.Config microProfileConfig = ConfigProvider.getConfig(); + parserCaptureIgnoredChars = microProfileConfig.getOptionalValue(ConfigKey.PARSER_CAPTURE_IGNORED_CHARS, + Boolean.class); + } + return parserCaptureIgnoredChars; + } + + @Override + public Optional isParserCaptureLineComments() { + if (parserCaptureLineComments == null) { + org.eclipse.microprofile.config.Config microProfileConfig = ConfigProvider.getConfig(); + parserCaptureLineComments = microProfileConfig.getOptionalValue(ConfigKey.PARSER_CAPTURE_LINE_COMMENTS, + Boolean.class); + } + return parserCaptureLineComments; + } + + @Override + public Optional isParserCaptureSourceLocation() { + if (parserCaptureSourceLocation == null) { + org.eclipse.microprofile.config.Config microProfileConfig = ConfigProvider.getConfig(); + parserCaptureSourceLocation = microProfileConfig.getOptionalValue(ConfigKey.PARSER_CAPTURE_SOURCE_LOCATION, + Boolean.class); + } + return parserCaptureSourceLocation; + } + + @Override + public Optional getParserMaxTokens() { + if (parserMaxTokens == null) { + org.eclipse.microprofile.config.Config microProfileConfig = ConfigProvider.getConfig(); + parserMaxTokens = microProfileConfig.getOptionalValue(ConfigKey.PARSER_MAX_TOKENS, Integer.class); + } + return parserMaxTokens; + } + + @Override + public Optional getParserMaxWhitespaceTokens() { + if (parserMaxWhitespaceTokens == null) { + org.eclipse.microprofile.config.Config microProfileConfig = ConfigProvider.getConfig(); + parserMaxWhitespaceTokens = microProfileConfig.getOptionalValue(ConfigKey.PARSER_MAX_WHITESPACE_TOKENS, + Integer.class); + } + return parserMaxWhitespaceTokens; + } + @Override public String getFieldVisibility() { if (fieldVisibility == null) { @@ -194,6 +250,26 @@ public Optional> getErrorExtensionFields() { return errorExtensionFields; } + @Override + public Optional getQueryComplexityInstrumentation() { + if (queryComplexityInstrumentation == null) { + org.eclipse.microprofile.config.Config microProfileConfig = ConfigProvider.getConfig(); + queryComplexityInstrumentation = microProfileConfig.getOptionalValue(ConfigKey.INSTRUMENTATION_QUERY_COMPLEXITY, + Integer.class); + } + return queryComplexityInstrumentation; + } + + @Override + public Optional getQueryDepthInstrumentation() { + if (queryDepthInstrumentation == null) { + org.eclipse.microprofile.config.Config microProfileConfig = ConfigProvider.getConfig(); + queryDepthInstrumentation = microProfileConfig.getOptionalValue(ConfigKey.INSTRUMENTATION_QUERY_DEPTH, + Integer.class); + } + return queryDepthInstrumentation; + } + @Override public T getConfigValue(String key, Class type, T defaultValue) { org.eclipse.microprofile.config.Config microProfileConfig = ConfigProvider.getConfig(); @@ -252,6 +328,26 @@ public void setLogPayload(LogPayloadOption logPayload) { this.logPayload = logPayload; } + public void setParserCaptureIgnoredChars(Optional parserCaptureIgnoredChars) { + this.parserCaptureIgnoredChars = parserCaptureIgnoredChars; + } + + public void setParserCaptureLineComments(Optional parserCaptureLineComments) { + this.parserCaptureLineComments = parserCaptureLineComments; + } + + public void setParserCaptureSourceLocation(Optional parserCaptureSourceLocation) { + this.parserCaptureSourceLocation = parserCaptureSourceLocation; + } + + public void setParserMaxTokens(Optional parserMaxTokens) { + this.parserMaxTokens = parserMaxTokens; + } + + public void setParserMaxWhitespaceTokens(Optional parserMaxWhitespaceTokens) { + this.parserMaxWhitespaceTokens = parserMaxWhitespaceTokens; + } + public void setFieldVisibility(String fieldVisibility) { this.fieldVisibility = fieldVisibility; } @@ -316,6 +412,14 @@ public void setIncludeIntrospectionTypesInSchema(Boolean includeIntrospectionTyp this.includeIntrospectionTypesInSchema = includeIntrospectionTypesInSchema; } + public void setQueryComplexityInstrumentation(Optional queryComplexityInstrumentation) { + this.queryComplexityInstrumentation = queryComplexityInstrumentation; + } + + public void getQueryDepthInstrumentation(Optional queryDepthInstrumentation) { + this.queryDepthInstrumentation = queryDepthInstrumentation; + } + private Optional> mergeList(Optional> currentList, Optional> deprecatedList) { List combined = new ArrayList<>(); @@ -333,10 +437,6 @@ private Optional> mergeList(Optional> currentList, Opt } } - private String getStringConfigValue(String key) { - return getStringConfigValue(key, null); - } - private String getStringConfigValue(String key, String defaultValue) { return getConfigValue(key, String.class, defaultValue); } diff --git a/server/implementation/src/main/java/io/smallrye/graphql/execution/ExecutionService.java b/server/implementation/src/main/java/io/smallrye/graphql/execution/ExecutionService.java index 2a472139d..a270f4fec 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/execution/ExecutionService.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/execution/ExecutionService.java @@ -2,6 +2,7 @@ import static io.smallrye.graphql.SmallRyeGraphQLServerLogging.log; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -20,9 +21,14 @@ import graphql.ExecutionInput.Builder; import graphql.ExecutionResult; import graphql.GraphQL; +import graphql.analysis.MaxQueryComplexityInstrumentation; +import graphql.analysis.MaxQueryDepthInstrumentation; import graphql.execution.ExecutionId; import graphql.execution.ExecutionStrategy; import graphql.execution.SubscriptionExecutionStrategy; +import graphql.execution.instrumentation.ChainedInstrumentation; +import graphql.execution.instrumentation.Instrumentation; +import graphql.parser.ParserOptions; import graphql.schema.GraphQLSchema; import io.smallrye.graphql.bootstrap.DataFetcherFactory; import io.smallrye.graphql.execution.context.SmallRyeContext; @@ -245,9 +251,24 @@ private DataLoaderRegistry getDataLoaderRegistry(List operatio private GraphQL getGraphQL() { if (this.graphQL == null) { if (graphQLSchema != null) { + Config config = Config.get(); + setParserOptions(config); + GraphQL.Builder graphqlBuilder = GraphQL.newGraphQL(graphQLSchema); graphqlBuilder = graphqlBuilder.defaultDataFetcherExceptionHandler(new ExceptionHandler()); - graphqlBuilder = graphqlBuilder.instrumentation(queryCache); + + List chainedList = new ArrayList<>(); + + if (config.getQueryComplexityInstrumentation().isPresent()) { + chainedList.add(new MaxQueryComplexityInstrumentation(config.getQueryComplexityInstrumentation().get())); + } + if (config.getQueryDepthInstrumentation().isPresent()) { + chainedList.add(new MaxQueryDepthInstrumentation(config.getQueryDepthInstrumentation().get())); + } + chainedList.add(queryCache); + // TODO: Allow users to add custome instumentations + graphqlBuilder = graphqlBuilder.instrumentation(new ChainedInstrumentation(chainedList)); + graphqlBuilder = graphqlBuilder.preparsedDocumentProvider(queryCache); if (queryExecutionStrategy != null) { @@ -273,4 +294,30 @@ private GraphQL getGraphQL() { return this.graphQL; } + + private void setParserOptions(Config config) { + if (config.hasParserOptions()) { + ParserOptions.Builder parserOptionsBuilder = ParserOptions.newParserOptions(); + if (config.isParserCaptureIgnoredChars().isPresent()) { + parserOptionsBuilder = parserOptionsBuilder + .captureIgnoredChars(config.isParserCaptureIgnoredChars().get()); + } + if (config.isParserCaptureLineComments().isPresent()) { + parserOptionsBuilder = parserOptionsBuilder + .captureLineComments(config.isParserCaptureLineComments().get()); + } + if (config.isParserCaptureSourceLocation().isPresent()) { + parserOptionsBuilder = parserOptionsBuilder + .captureSourceLocation(config.isParserCaptureSourceLocation().get()); + } + if (config.getParserMaxTokens().isPresent()) { + parserOptionsBuilder = parserOptionsBuilder.maxTokens(config.getParserMaxTokens().get()); + } + if (config.getParserMaxWhitespaceTokens().isPresent()) { + parserOptionsBuilder = parserOptionsBuilder + .maxWhitespaceTokens(config.getParserMaxWhitespaceTokens().get()); + } + ParserOptions.setDefaultParserOptions(parserOptionsBuilder.build()); + } + } } diff --git a/server/implementation/src/main/java/io/smallrye/graphql/spi/config/Config.java b/server/implementation/src/main/java/io/smallrye/graphql/spi/config/Config.java index 6d4fa4b44..c52d4e391 100644 --- a/server/implementation/src/main/java/io/smallrye/graphql/spi/config/Config.java +++ b/server/implementation/src/main/java/io/smallrye/graphql/spi/config/Config.java @@ -163,6 +163,42 @@ default LogPayloadOption logPayload() { return LogPayloadOption.off; } + default Optional isParserCaptureIgnoredChars() { + return Optional.empty(); + } + + default Optional isParserCaptureLineComments() { + return Optional.empty(); + } + + default Optional isParserCaptureSourceLocation() { + return Optional.empty(); + } + + default Optional getParserMaxTokens() { + return Optional.empty(); + } + + default Optional getParserMaxWhitespaceTokens() { + return Optional.empty(); + } + + default boolean hasParserOptions() { + return isParserCaptureIgnoredChars().isPresent() + || isParserCaptureLineComments().isPresent() + || isParserCaptureSourceLocation().isPresent() + || getParserMaxTokens().isPresent() + || getParserMaxWhitespaceTokens().isPresent(); + } + + default Optional getQueryComplexityInstrumentation() { + return Optional.empty(); + } + + default Optional getQueryDepthInstrumentation() { + return Optional.empty(); + } + default String getFieldVisibility() { return FIELD_VISIBILITY_DEFAULT; } diff --git a/server/tck/src/test/resources/META-INF/microprofile-config.properties b/server/tck/src/test/resources/META-INF/microprofile-config.properties index e8ffdb645..ad7b91b3f 100644 --- a/server/tck/src/test/resources/META-INF/microprofile-config.properties +++ b/server/tck/src/test/resources/META-INF/microprofile-config.properties @@ -5,4 +5,5 @@ smallrye.graphql.logPayload=true mp.graphql.showErrorMessage=java.security.AccessControlException,io.smallrye.graphql.test.apps.exceptionlist.* mp.graphql.hideErrorMessage=java.io.IOException,io.smallrye.graphql.test.apps.exceptionlist.* -smallrye.graphql.errorExtensionFields=exception,classification,code,description,validationErrorType,queryPath \ No newline at end of file +smallrye.graphql.errorExtensionFields=exception,classification,code,description,validationErrorType,queryPath +smallrye.graphql.parser.capture.sourceLocation=false \ No newline at end of file diff --git a/server/tck/src/test/resources/overrides/createNewNullNamedHero/output2.json b/server/tck/src/test/resources/overrides/createNewNullNamedHero/output2.json new file mode 100644 index 000000000..068ed46a7 --- /dev/null +++ b/server/tck/src/test/resources/overrides/createNewNullNamedHero/output2.json @@ -0,0 +1,14 @@ +{ + "errors": [ + { + "message": "Validation error (WrongType@[createNewHero]) : argument 'hero.name' with value 'NullValue{}' must not be null", + "locations": [ + { + "line": 2, + "column": 19 + } + ] + } + ], + "data": null +} \ No newline at end of file diff --git a/server/tck/src/test/resources/overrides/createNewUnnamedHero/output2.json b/server/tck/src/test/resources/overrides/createNewUnnamedHero/output2.json new file mode 100644 index 000000000..ea8331aa7 --- /dev/null +++ b/server/tck/src/test/resources/overrides/createNewUnnamedHero/output2.json @@ -0,0 +1,14 @@ +{ + "errors": [ + { + "message": "Validation error (WrongType@[createNewHero]) : argument 'hero' with value 'ObjectValue{objectFields=[ObjectField{name='realName', value=StringValue{value='John Smith'}}]}' is missing required fields '[name]'", + "locations": [ + { + "line": 2, + "column": 19 + } + ] + } + ], + "data": null +} \ No newline at end of file diff --git a/server/tck/src/test/resources/overrides/invalidDataTypeValue/output3.json b/server/tck/src/test/resources/overrides/invalidDataTypeValue/output3.json new file mode 100644 index 000000000..5f9b23eab --- /dev/null +++ b/server/tck/src/test/resources/overrides/invalidDataTypeValue/output3.json @@ -0,0 +1,16 @@ +{ + "errors": [ + { + "message": "argument 'powerLevel' with value 'StringValue{value='Unlimited'}' is not a valid 'Int'", + "locations": [ + { + "line": 2, + "column": 37 + } + ] + } + ], + "data": { + "updateItemPowerLevel": null + } +} diff --git a/server/tck/src/test/resources/overrides/invalidEnumValue/output2.json b/server/tck/src/test/resources/overrides/invalidEnumValue/output2.json new file mode 100644 index 000000000..a667fb5d6 --- /dev/null +++ b/server/tck/src/test/resources/overrides/invalidEnumValue/output2.json @@ -0,0 +1,14 @@ +{ + "errors": [ + { + "message": "Validation error (WrongType@[createNewHero]) : argument 'hero.tshirtSize' with value 'EnumValue{name='XLTall'}' is not a valid 'ShirtSize' - Expected enum literal value not in allowable values - 'EnumValue{name='XLTall'}'.", + "locations": [ + { + "line": 3, + "column": 19 + } + ] + } + ], + "data": null +} \ No newline at end of file diff --git a/server/tck/src/test/resources/overrides/invalidLocalDateFormattedValue/output3.json b/server/tck/src/test/resources/overrides/invalidLocalDateFormattedValue/output3.json new file mode 100644 index 000000000..4fcff2375 --- /dev/null +++ b/server/tck/src/test/resources/overrides/invalidLocalDateFormattedValue/output3.json @@ -0,0 +1,16 @@ +{ + "errors": [ + { + "message": "argument 'date' with value 'StringValue{value='Today'}' is not a valid 'Date'", + "locations": [ + { + "line": 2, + "column": 49 + } + ] + } + ], + "data": { + "checkInWithCorrectDateFormat": null + } +} diff --git a/server/tck/src/test/resources/overrides/invalidLocalDateTimeFormattedValue/output3.json b/server/tck/src/test/resources/overrides/invalidLocalDateTimeFormattedValue/output3.json new file mode 100644 index 000000000..34be441fa --- /dev/null +++ b/server/tck/src/test/resources/overrides/invalidLocalDateTimeFormattedValue/output3.json @@ -0,0 +1,16 @@ +{ + "errors": [ + { + "message": "argument 'dateTime' with value 'StringValue{value='Today'}' is not a valid 'DateTime'", + "locations": [ + { + "line": 2, + "column": 48 + } + ] + } + ], + "data": { + "battleWithCorrectDateFormat": null + } +} \ No newline at end of file diff --git a/server/tck/src/test/resources/overrides/invalidLocalDateTimeValue/output3.json b/server/tck/src/test/resources/overrides/invalidLocalDateTimeValue/output3.json new file mode 100644 index 000000000..af2c0dc4e --- /dev/null +++ b/server/tck/src/test/resources/overrides/invalidLocalDateTimeValue/output3.json @@ -0,0 +1,16 @@ +{ + "errors": [ + { + "message": "argument 'dateTime' with value 'StringValue{value='Today'}' is not a valid 'DateTime'", + "locations": [ + { + "line": 2, + "column": 27 + } + ] + } + ], + "data": { + "battle": null + } +} \ No newline at end of file diff --git a/server/tck/src/test/resources/overrides/invalidLocalDateValue/output3.json b/server/tck/src/test/resources/overrides/invalidLocalDateValue/output3.json new file mode 100644 index 000000000..49fac88d8 --- /dev/null +++ b/server/tck/src/test/resources/overrides/invalidLocalDateValue/output3.json @@ -0,0 +1,16 @@ +{ + "errors": [ + { + "message": "argument 'date' with value 'StringValue{value='Today'}' is not a valid 'Date'", + "locations": [ + { + "line": 2, + "column": 28 + } + ] + } + ], + "data": { + "checkIn": null + } +} diff --git a/server/tck/src/test/resources/overrides/invalidLocalTimeFormattedValue/output3.json b/server/tck/src/test/resources/overrides/invalidLocalTimeFormattedValue/output3.json new file mode 100644 index 000000000..dbd808830 --- /dev/null +++ b/server/tck/src/test/resources/overrides/invalidLocalTimeFormattedValue/output3.json @@ -0,0 +1,16 @@ +{ + "errors": [ + { + "message": "argument 'time' with value 'StringValue{value='Today'}' is not a valid 'Time'", + "locations": [ + { + "line": 2, + "column": 57 + } + ] + } + ], + "data": { + "startPatrollingWithCorrectDateFormat": null + } +} diff --git a/server/tck/src/test/resources/overrides/invalidLocalTimeValue/output3.json b/server/tck/src/test/resources/overrides/invalidLocalTimeValue/output3.json new file mode 100644 index 000000000..78d6bfd37 --- /dev/null +++ b/server/tck/src/test/resources/overrides/invalidLocalTimeValue/output3.json @@ -0,0 +1,16 @@ +{ + "errors": [ + { + "message": "argument 'time' with value 'StringValue{value='Today'}' is not a valid 'Time'", + "locations": [ + { + "line": 2, + "column": 36 + } + ] + } + ], + "data": { + "startPatrolling": null + } +} \ No newline at end of file diff --git a/server/tck/src/test/resources/overrides/unknownField/output2.json b/server/tck/src/test/resources/overrides/unknownField/output2.json new file mode 100644 index 000000000..6715517fc --- /dev/null +++ b/server/tck/src/test/resources/overrides/unknownField/output2.json @@ -0,0 +1,14 @@ +{ + "errors": [ + { + "message": "Validation error (FieldUndefined@[allHeroes/weaknesses]) : Field 'weaknesses' in type 'SuperHero' is undefined", + "locations": [ + { + "line": 4, + "column": 5 + } + ] + } + ], + "data": null +} \ No newline at end of file diff --git a/server/tck/src/test/resources/overrides/unknownMutation/output2.json b/server/tck/src/test/resources/overrides/unknownMutation/output2.json new file mode 100644 index 000000000..c63695ca0 --- /dev/null +++ b/server/tck/src/test/resources/overrides/unknownMutation/output2.json @@ -0,0 +1,14 @@ +{ + "errors": [ + { + "message": "Validation error (FieldUndefined@[createNewHeroCat]) : Field 'createNewHeroCat' in type 'Mutation' is undefined", + "locations": [ + { + "line": 2, + "column": 5 + } + ] + } + ], + "data": null +} \ No newline at end of file diff --git a/server/tck/src/test/resources/overrides/unknownQuery/output2.json b/server/tck/src/test/resources/overrides/unknownQuery/output2.json new file mode 100644 index 000000000..474c2b5d5 --- /dev/null +++ b/server/tck/src/test/resources/overrides/unknownQuery/output2.json @@ -0,0 +1,14 @@ +{ + "errors": [ + { + "message": "Validation error (FieldUndefined@[allHeroesWhoLikeIceCream]) : Field 'allHeroesWhoLikeIceCream' in type 'Query' is undefined", + "locations": [ + { + "line": 2, + "column": 3 + } + ] + } + ], + "data": null +}