graphql-platform icon indicating copy to clipboard operation
graphql-platform copied to clipboard

Add the ability to mark HotChocolate Generated types with directives to support federation

Open tcodd86 opened this issue 1 year ago • 4 comments

Product

Hot Chocolate

Is your feature request related to a problem?

When using the UsePaging attribute on queries there is a type, PageInfo, that is generated and added to the schema. In federated graphs on Federation v2, shared value types in sub-graphs must be tagged with the @shareable directive or the subgraph schema will fail the subgraph check with an error similar to what's below.

error[E029]: Encountered 4 build errors while trying to build subgraph "my-subgraph" into supergraph "my-supergraph".
Caused by:

    INVALID_FIELD_SHARING: Non-shareable field "PageInfo.hasNextPage" is resolved from multiple subgraphs: it is resolved from subgraphs "x", "y", and "z" and defined as non-shareable in subgraph "my-subgraph"
    INVALID_FIELD_SHARING: Non-shareable field "PageInfo.hasPreviousPage" is resolved from multiple subgraphs: it is resolved from subgraphs "x", "y", and "z" and defined as non-shareable in subgraph "my-subgraph"
    INVALID_FIELD_SHARING: Non-shareable field "PageInfo.startCursor" is resolved from multiple subgraphs: it is resolved from subgraphs "x", "y", and "z" and defined as non-shareable in subgraph "my-subgraph"
    INVALID_FIELD_SHARING: Non-shareable field "PageInfo.endCursor" is resolved from multiple subgraphs: it is resolved from subgraphs "x", "y", and "z" and defined as non-shareable in subgraph "my-subgraph"

The solution you'd like

I would like the HotChocolate library to support adding the shareable directive to HotChocolate framework generated types, such as PageInfo, so that they can be used in federated schemas on Federation v2.

tcodd86 avatar Jul 23 '24 16:07 tcodd86

It's a bit too late for 14 to add this ... but I will add it to the 14.1 backlog

michaelstaib avatar Jul 25 '24 04:07 michaelstaib

Is there any workaround so far to rename the PageInfo type to avoid the conflicts in a federation scenario?

rohinz avatar Aug 14 '24 09:08 rohinz

I was able to get around this by writing a custom type interceptor to add the shareable directive to the PageInfo type. However in order to get the @shareable directive imported correctly in the @link directive I had to add a dummy type with a field marked as shareable using the HotChocolate extension method.

public override void OnBeforeCompleteType(ITypeCompletionContext completionContext, DefinitionBase definition)
{
    if (definition is ObjectTypeDefinition otd)
    {
        switch (definition.Name)
        {
            case "PageInfo":
                otd.AddDirective("shareable", completionContext.TypeInspector);
                break;
        }
    }
    base.OnBeforeCompleteType(completionContext, definition);
}

tcodd86 avatar Aug 14 '24 14:08 tcodd86

Yes ... a type interceptor is also how we will implement it in the core.

michaelstaib avatar Aug 19 '24 05:08 michaelstaib

I was able to get around this by writing a custom type interceptor to add the shareable directive to the PageInfo type. However in order to get the @shareable directive imported correctly in the @link directive I had to add a dummy type with a field marked as shareable using the HotChocolate extension method.

public override void OnBeforeCompleteType(ITypeCompletionContext completionContext, DefinitionBase definition)
{
    if (definition is ObjectTypeDefinition otd)
    {
        switch (definition.Name)
        {
            case "PageInfo":
                otd.AddDirective("shareable", completionContext.TypeInspector);
                break;
        }
    }
    base.OnBeforeCompleteType(completionContext, definition);
}

Thanks a lot for sharing this! Could you also share the rest of it where you added the dummy type, etc? Thanks!

taipignas avatar Sep 17 '24 07:09 taipignas

I tried to use the interceptor

    public class ShareableDirectiveTypeInterceptor : TypeInterceptor
    {
        public override void OnBeforeCompleteType(
            ITypeCompletionContext completionContext,
            DefinitionBase definition)
        {
            if (definition is ObjectTypeDefinition otd)
            {
                switch (definition.Name)
                {
                    case "PageInfo":
                        otd.AddDirective("shareable", completionContext.TypeInspector);
                        break;
                }
            }
            base.OnBeforeCompleteType(completionContext, definition);
        }
    }

but got:

The specified directive `@shareable` is not allowed on the current location `Object`. (HotChocolate.Types.Pagination.PageInfoType)

AdamDurkalec avatar Sep 17 '24 12:09 AdamDurkalec

I was able to get around this by writing a custom type interceptor to add the shareable directive to the PageInfo type. However in order to get the @shareable directive imported correctly in the @link directive I had to add a dummy type with a field marked as shareable using the HotChocolate extension method.

public override void OnBeforeCompleteType(ITypeCompletionContext completionContext, DefinitionBase definition)
{
    if (definition is ObjectTypeDefinition otd)
    {
        switch (definition.Name)
        {
            case "PageInfo":
                otd.AddDirective("shareable", completionContext.TypeInspector);
                break;
        }
    }
    base.OnBeforeCompleteType(completionContext, definition);
}

Thanks a lot for sharing this! Could you also share the rest of it where you added the dummy type, etc? Thanks!

Here is a dummy type similar to what we used to get the shareable directive properly imported into the link directive. This type will be added to your graph, but by marking it inaccessible clients will not be able to query for it. Once HotChocolate supports marking PageInfo and other types as shareable we will remove this type from our graph.

```csharp
public class ShareableDirectiveLoaderType : ObjectType
{
    public ShareableDirectiveLoaderType()
        : base(descriptor =>
        {
            descriptor.Name("ShareableDirectiveLoader")
                .Description("This type is only used to make HotChocolate .NET APIs import the Shareable directive from Apollo into their sub-graph. It is marked with the inaccessible directive to prevent clients from querying it.")
                .Inaccessible();

            descriptor.Field("ShareableField")
                .Description(@"This field is tagged with HotChocolate's .Shareable() extension method. This makes HotChocolate import the shareable directive from Apollo and add it to the link directive. The field will also be marked with the shareable directive.")
                .Type<StringType>()
                .Shareable()
                // this defines the resolver within the type
                .Resolve(context =>
                {
                    return "Shared field.";
                });
        })
    {
    }
}

tcodd86 avatar Sep 17 '24 14:09 tcodd86

Here is a very easy way to add the @shareable directive to the PageInfo type without any hacks:

public class PageInfoType : ObjectTypeExtension
{
    protected override void Configure(IObjectTypeDescriptor descriptor)
    {
        descriptor.Name("PageInfo");
        descriptor.Shareable();
    }
}

Just register the type extension:

public static IRequestExecutorBuilder ConfigureGraphqlSchema(this IRequestExecutorBuilder builder) =>
        builder
            .AddApolloFederation()
            .AddQueryType(ConfigureQueryType)
            .AddTypeExtension<PageInfoType>();

rohinz avatar Sep 18 '24 07:09 rohinz

@rohinz good point ... often simple type extensions are the simplest way to do these things :)

michaelstaib avatar Sep 18 '24 18:09 michaelstaib

This is now implemented

michaelstaib avatar Sep 20 '24 14:09 michaelstaib

https://github.com/ChilliCream/graphql-platform/releases/tag/14.0.0-p.163

michaelstaib avatar Sep 20 '24 16:09 michaelstaib