Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updating Federation directives #2010

Merged
merged 2 commits into from
Jan 30, 2024
Merged

Conversation

RoKKim
Copy link
Contributor

@RoKKim RoKKim commented Jan 20, 2024

@RoKKim
Copy link
Contributor Author

RoKKim commented Jan 20, 2024

Will check the test results.

@RoKKim
Copy link
Contributor Author

RoKKim commented Jan 20, 2024

I had to implement the following methods due to the upgrade of federation-graphql-java-support:

  • io.smallrye.graphql.scalar.number.NumberCoercing#valueToLiteral
  • io.smallrye.graphql.scalar.time.DateCoercing#valueToLiteral

Without these implementations, errors were produced during the execution of CdiExecutionTest:

2024-01-20T18:12:06.1113496Z [ERROR] io.smallrye.graphql.execution.SchemaTest.testSchemaModelCreation -- Time elapsed: 0.102 s <<< ERROR!
2024-01-20T18:12:06.1114956Z java.lang.UnsupportedOperationException: The non deprecated version of valueToLiteral has not been implemented by this scalar : class io.smallrye.graphql.scalar.number.NumberCoercing
2024-01-20T18:12:06.1116133Z 	at graphql.schema.Coercing.valueToLiteral(Coercing.java:228)
2024-01-20T18:12:06.1116678Z 	at graphql.schema.Coercing.valueToLiteral(Coercing.java:246)
2024-01-20T18:12:06.1117585Z 	at graphql.execution.ValuesResolverConversion.externalValueToLiteralForScalar(ValuesResolverConversion.java:225)
2024-01-20T18:12:06.1118641Z 	at graphql.execution.ValuesResolverConversion.externalValueToLiteral(ValuesResolverConversion.java:184)
2024-01-20T18:12:06.1119641Z 	at graphql.execution.ValuesResolverConversion.externalValueToLiteral(ValuesResolverConversion.java:175)
2024-01-20T18:12:06.1120684Z 	at graphql.execution.ValuesResolverConversion.externalValueToLiteralForObject(ValuesResolverConversion.java:318)
2024-01-20T18:12:06.1121731Z 	at graphql.execution.ValuesResolverConversion.externalValueToLiteral(ValuesResolverConversion.java:204)
2024-01-20T18:12:06.1122713Z 	at graphql.execution.ValuesResolverConversion.valueToLiteralImpl(ValuesResolverConversion.java:88)
2024-01-20T18:12:06.1123546Z 	at graphql.execution.ValuesResolver.valueToLiteral(ValuesResolver.java:248)
2024-01-20T18:12:06.1124207Z 	at graphql.schema.idl.SchemaPrinter.printAst(SchemaPrinter.java:718)
2024-01-20T18:12:06.1124844Z 	at graphql.schema.idl.SchemaPrinter.argsString(SchemaPrinter.java:811)
2024-01-20T18:12:06.1125570Z 	at graphql.schema.idl.SchemaPrinter.lambda$printFieldDefinitions$4(SchemaPrinter.java:536)
2024-01-20T18:12:06.1126405Z 	at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:183)
2024-01-20T18:12:06.1127084Z 	at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
2024-01-20T18:12:06.1127697Z 	at java.base/java.util.stream.SortedOps$RefSortingSink.end(SortedOps.java:395)
2024-01-20T18:12:06.1128348Z 	at java.base/java.util.stream.Sink$ChainedReference.end(Sink.java:258)
2024-01-20T18:12:06.1129501Z 	at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:485)
2024-01-20T18:12:06.1130305Z 	at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:474)
2024-01-20T18:12:06.1131129Z 	at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:150)
2024-01-20T18:12:06.1132104Z 	at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:173)
2024-01-20T18:12:06.1132904Z 	at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
2024-01-20T18:12:06.1133663Z 	at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:497)
2024-01-20T18:12:06.1134438Z 	at graphql.schema.idl.SchemaPrinter.printFieldDefinitions(SchemaPrinter.java:532)
2024-01-20T18:12:06.1135189Z 	at graphql.schema.idl.SchemaPrinter.lambda$objectPrinter$8(SchemaPrinter.java:642)
2024-01-20T18:12:06.1135928Z 	at graphql.schema.idl.SchemaPrinter.printSchemaElement(SchemaPrinter.java:1018)
2024-01-20T18:12:06.1136611Z 	at graphql.schema.idl.SchemaPrinter.print(SchemaPrinter.java:449)
2024-01-20T18:12:06.1137521Z 	at com.apollographql.federation.graphqljava.printer.ServiceSDLPrinter.generateServiceSDLV2(ServiceSDLPrinter.java:140)
2024-01-20T18:12:06.1138585Z 	at com.apollographql.federation.graphqljava.SchemaTransformer.build(SchemaTransformer.java:110)
2024-01-20T18:12:06.1139451Z 	at io.smallrye.graphql.bootstrap.Bootstrap.generateGraphQLSchema(Bootstrap.java:212)
2024-01-20T18:12:06.1140195Z 	at io.smallrye.graphql.bootstrap.Bootstrap.bootstrap(Bootstrap.java:126)
2024-01-20T18:12:06.1140948Z 	at io.smallrye.graphql.execution.SchemaTest.testSchemaModelCreation(SchemaTest.java:39)
2024-01-20T18:12:06.1141647Z 	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
2024-01-20T18:12:06.1142180Z 	at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
2024-01-20T18:12:06.1142701Z 	at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)

I believe these valueToLiteral methods should cover most of the cases.

@RoKKim RoKKim mentioned this pull request Jan 20, 2024
@jmartisk
Copy link
Member

Hmm, the @requiresScopes - and also @policy - directives will be quite tricky due to the 2D array.
Also I think another problem with them is that the values of that array need to be a special scalar type named federation__Scope and federation__Policy (see spec) and we currently have no straightforward way to have custom scalars as arguments of directives.

A rather crazy idea that I have is that

But if it turns out to be too hacky and unwieldy, I'd say we shouldn't force-fit it... I'm not sure how much these things are in use, and whether it's absolutely necessary to support them at the cost of introducing ugly hacks.

@jmartisk
Copy link
Member

As for the change you've introduced that now shows the default values of directive arguments if they're not explicitly specified, I think that's a good idea. While AFAIU it's not strictly necessary, I guess it will help clear out some confusion.

@@ -87,6 +89,7 @@ private IndexView createCustomIndex() {
indexer.index(convertClassToInputStream(Repeatable.class));

// directives from the API module
indexer.index(convertClassToInputStream(Authenticated.class));
Copy link
Member

@jmartisk jmartisk Jan 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note mostly to myself: the same change will also be needed in Quarkus (https://github.com/quarkusio/quarkus/blob/3.7.0.CR1/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLProcessor.java#L275) - I'll take care of that after we release SR-GQL with this change

@RoKKim
Copy link
Contributor Author

RoKKim commented Jan 22, 2024

Hmm, the @requiresScopes - and also @policy - directives will be quite tricky due to the 2D array. Also I think another problem with them is that the values of that array need to be a special scalar type named federation__Scope and federation__Policy (see spec) and we currently have no straightforward way to have custom scalars as arguments of directives.

A rather crazy idea that I have is that

But if it turns out to be too hacky and unwieldy, I'd say we shouldn't force-fit it... I'm not sure how much these things are in use, and whether it's absolutely necessary to support them at the cost of introducing ugly hacks.

Sounds like this is doable. I will give it a go as soon as I can.

@RoKKim
Copy link
Contributor Author

RoKKim commented Jan 27, 2024

I managed to get this working with the following type of directive (example for @Policy):

@Policy(policies = {@PolicyGroup({"policy1", "policy2"}), @PolicyGroup({"policy3"})})

I believe this approach is better than allowing users to define an array of strings separated by commas:

@Policy(policies = {"policy1, policy2", "policy3"})

Now, achieving all of this isn't actually that hacky:

  1. In io.smallrye.graphql.schema.creator.DirectiveTypeCreator#create, I overwrite the argumentType for both @Policy and @RequiresScopes, and add NON_NULL annotation to all levels, since this is the directive declaration:

    directive @policy(policies: [[Policy!]!]!)
    scalar Policy

    Here is the current state of io.smallrye.graphql.schema.model.DirectiveArgument:
    image
    For now we still use String instead of Policy (which is just scalar), as we do for @Key, @Requires and @Provides:

    directive @key(fields: FieldSet!, resolvable: Boolean = true)
    directive @requires(fields: FieldSet!)
    directive @provides(fields: FieldSet!)
    scalar FieldSet

    So if I understand the io.smallrye.graphql.schema.model.DirectiveArgument class correctly, we have:

    • a base type of String, for which the property notNull must be set to true beacuse of Policy!
    • a first wrapper around it, for which the property notEmpty must be set to true
    • a second wrapper around it, for which the property notEmpty must be set to true
  2. In io.smallrye.graphql.schema.helper.Directives#toDirectiveInstance I parse the values of @PolicyGroup and @ScopeGroup annotations and save them into a nested list List<List<String>>.

  3. To simplify the process, I used method io.smallrye.graphql.bootstrap.Bootstrap#createGraphQLInputType inside io.smallrye.graphql.bootstrap.Bootstrap#createGraphQLDirectiveType, as it already handles conversion of wrappers and I didn't want to duplicate the code.
    Here is where I discovered a potential bug: the method in its current implementation does not correctly handle the nonNull attribute. For our case, which involves the type [[String!]!]! (since we currently use String instead of Policy), the method incorrectly generated [[String!]]!. I suspect this problem hasn't been identified because we haven't had to deal with such cases before.

Now my schema is successfuly generated:

directive @policy(policies: [[String!]!]!) on SCALAR | OBJECT | FIELD_DEFINITION | INTERFACE | ENUM
type Product @authenticated @key(fields : "id")@policy(policies : [["policy1", "policy2"], ["policy3"]]) @requiresScopes(scopes : [["scope1", "scope2"], ["scope3"]]) {
  ...
}

However, a problem arose during testing. When running the test io.smallrye.graphql.execution.CdiExecutionTest#testInputWithDifferentNameOnInputAndType, I encountered the following error:

INFO: {"errors":[{"message":"Validation error (WrongType@[createNewHero]) : argument 'hero' with value 'ObjectValue{objectFields=[ObjectField{name='realName', value=StringValue{value='Steven Rogers'}}, ObjectField{name='name', value=StringValue{value='Captain America'}}, ObjectField{name='dateOfLastCheckin', value=StringValue{value='09/25/2019'}}, ObjectField{name='patrolStartTime', value=StringValue{value='13:00'}}, ObjectField{name='timeOfLastBattle', value=StringValue{value='09:43:23 21-08-2019'}}, ObjectField{name='tshirtSize', value=EnumValue{name='XL'}}]}' is missing required fields '[equipment, superPowers]'","locations":[{"line":2,"column":19}],

This is the object org.eclipse.microprofile.graphql.tck.apps.superhero.model.SuperHero we use for testing:

public class SuperHero implements Character {
    private List<Team> teamAffiliations;
    private List<@NonNull String> superPowers;

Therefore, the GraphQL type should be [String!].
Considering the first point of how notNull and notEmpty should reflect in GraphQL types and the fixes I have implemented, the type I get with my changes is [String]!, since this is the state of superPowers field:
image

So my question is, in the context of handling notNull and notEmpty properties, should the io.smallrye.graphql.schema.creator.WrapperCreator class return true for notNull on the String reference and false for true on the List wrapper?

@@ -46,12 +45,36 @@ public DirectiveType create(ClassInfo classInfo) {
directiveType.setLocations(getLocations(classInfo.declaredAnnotation(DIRECTIVE)));
directiveType.setRepeatable(classInfo.hasAnnotation(Annotations.REPEATABLE));

Class<?> directiveClass = null;
try {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please no loading of application classes in the schema-builder module - this happens at build time, and loading application classes may lead to a lot problems especially with native mode.
I see you're using this for checking the directive's type, can you just use something like

// create these just once, as a constant
DotName POLICY = DotName.createSimple(Policy.class.getName());
DotName REQUIRES_SCOPES = DotName.createSimple(RequiresScopes.class.getName());
(...)
if(classInfo.name().equals(POLICY) || classInfo.name().equals(REQUIRES_SCOPES)) {
  ...
}

?

I guess you don't have to deal with inheritance here because Java annotations can't inherit.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestion, fixed with static final variables:
a55b152#diff-5270b78e6efcb5bcd4deb9ab5d9532e4e3e8fed016364e918a7bd48accbda320R30

import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;
import org.jboss.jandex.MethodInfo;
import org.jboss.jandex.*;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please avoid star imports

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// of strings, where none of the nested elements can be null
AnnotationInstance nonNullAnnotation = AnnotationInstance.create(NON_NULL, null,
Collections.emptyList());
DotName stringDotName = DotName.createSimple(String.class.getName());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be pulled up into a constant so we only create this DotName once

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for (AnnotationValue annotationValue : annotationInstance.values()) {
directiveInstance.setValue(annotationValue.name(), valueObject(annotationValue));
if (RequiresScopes.class.isAssignableFrom(directiveClass) || Policy.class.isAssignableFrom(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again,

DotName POLICY = DotName.createSimple(Policy.class.getName());
DotName REQUIRES_SCOPES = DotName.createSimple(RequiresScopes.class.getName());
if(classInfo.name().equals(POLICY) || classInfo.name().equals(REQUIRES_SCOPES)) {
  ...
}

should do It

Copy link
Contributor Author

@RoKKim RoKKim Jan 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, I resolved this by using Policy.class.getName(), since DirectiveType#name is stored as string, not as DotName.
a55b152#diff-a9f696258582406ccbe182f6ecafe672d772737c348aafb1e843d62d3f76e722R70

do {
if (wrapper.isCollectionOrArrayOrMap()) {
graphQLInputType = list(graphQLInputType);
// Wrapper itself can also be mandatory
if (wrapper.isNotEmpty()) {
graphQLInputType = GraphQLNonNull.nonNull(graphQLInputType);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I think the wrapper.isNotEmpty() method has a rather confusing name and it should instead be isWrappedTypeNotNull() - it refers to non-nullability of the wrapped type, not the wrapper itself. Therefore moving this graphQLInputType = GraphQLNonNull.nonNull(graphQLInputType); after graphQLInputType = list(graphQLInputType) is wrong because then you mark the OUTER type (the wrapper) as non-null based on the INNER type being non-null.

I think we should rename isNotEmpty to isWrappedTypeNotNull and then here we would have this method as follows:

private GraphQLInputType createGraphQLInputType(Field field) {
        GraphQLInputType graphQLInputType = referenceGraphQLInputType(field);

        Wrapper wrapper = dataFetcherFactory.unwrap(field, false);
        // Field can have a wrapper, like List<String>
        if (wrapper != null && wrapper.isCollectionOrArrayOrMap()) {
            // Loop as long as there is a wrapper
            do {
                if (wrapper.isCollectionOrArrayOrMap()) {
                    if (wrapper.isWrappedTypeNotNull()) {
                        graphQLInputType = GraphQLNonNull.nonNull(graphQLInputType);
                    }
                    graphQLInputType = list(graphQLInputType);
                    wrapper = wrapper.getWrapper();
                } else {
                    wrapper = null;
                }
            } while (wrapper != null);
        }

        // Check if field is mandatory
        if (field.isNotNull()) {
            graphQLInputType = GraphQLNonNull.nonNull(graphQLInputType);
        }

        return graphQLInputType;
    }

??? The tests pass for me then. But I may not properly understand the reason for your changes here.

AFAIU there's no concept of not-empty collections in GraphQL. If the inner type is marked as not-null, it means that the collection must not contain null entries, NOT that it must not be empty.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe @phillip-kruger remembers details about the wrapper.isNotEmpty method and can correct me, but I think it means that the wrapped type is not-null

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes a lot more sense, thank you for the explanation. I implemented the fixes you mentioned and now the tests pass successfully.
f0bd744

@jmartisk
Copy link
Member

Kudos for the idea with ScopeGroup and PolicyGroup, that's definitely nicer than my original idea with parsing arrays from strings

@jmartisk jmartisk force-pushed the issue-federation-2.5 branch from fcbf5fb to 0f3887e Compare January 30, 2024 11:28
@jmartisk jmartisk force-pushed the issue-federation-2.5 branch from 0f3887e to b7331c3 Compare January 30, 2024 11:33
@jmartisk
Copy link
Member

I've rebase, squashed the commits and added one more little fix, hopefully it's all good
After merging, I'd create an issue for fixing the minor issue where the directive arguments are declared to be String:

directive @requiresScopes(scopes: [[String!]!]!)

This might become a more general fix because we don't have straightforward support for custom scalars in directive arguments

@RoKKim
Copy link
Contributor Author

RoKKim commented Jan 30, 2024

I've rebase, squashed the commits and added one more little fix, hopefully it's all good After merging, I'd create an issue for fixing the minor issue where the directive arguments are declared to be String:

directive @requiresScopes(scopes: [[String!]!]!)

This might become a more general fix because we don't have straightforward support for custom scalars in directive arguments

I started the implementation of supporting directive @link as well, which uses scalar Import. It can be either a string or an object so I will have to check the posibility of implementing this either way.

@jmartisk
Copy link
Member

I started the implementation of supporting directive @link as well, which uses scalar Import. It can be either a string or an object so I will have to check the posibility of implementing this either way.

Nice! Let's do it in a separate PR please. Together with some other PRs that are going in right now, it's becoming a bit too much to handle for me.

@jmartisk jmartisk merged commit 3a821bb into smallrye:main Jan 30, 2024
5 checks passed
@jmartisk
Copy link
Member

Thanks for your contribution, this is awesome

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants