Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using GreenDonut.Data;
using HotChocolate.ApolloFederation.Resolvers;
using HotChocolate.ApolloFederation.Types;
using HotChocolate.Configuration;
Expand All @@ -17,6 +18,8 @@ namespace HotChocolate.ApolloFederation;

internal sealed class FederationTypeInterceptor : TypeInterceptor
{
private const string PageCursorTypeName = "PageCursor";

private static readonly MethodInfo s_matches =
typeof(ReferenceResolverHelper)
.GetMethod(
Expand Down Expand Up @@ -117,23 +120,20 @@ public override IEnumerable<TypeReference> RegisterMoreTypes(
yield return _typeInspector.GetTypeRef(typeof(LinkDirective));
}

// The page info fields are made shareable in OnAfterCompleteName for any type
// named "PageInfo". That match is by name, so it also covers the page info type
// that is inferred from PageConnection<T> (runtime type PageInfo) and not just the
// built-in PageInfoType. We therefore have to register the shareable directive for
// any discovered page info object type, otherwise the directive cannot be resolved
// when the fields are completed.
if (discoveryContexts.Any(t => IsPageInfoType(t.Type))
// PageInfo and PageCursor are marked @shareable in OnBeforeCompleteType, which
// requires the directive type to be registered when either of them is discovered.
if (discoveryContexts.Any(t => IsPagingType(t.Type))
&& discoveryContexts.All(t => t.Type is not DirectiveType<ShareableDirective>))
{
yield return _typeInspector.GetTypeRef(typeof(ShareableDirective));
}
}

private static bool IsPageInfoType(TypeSystemObject type)
private static bool IsPagingType(TypeSystemObject type)
=> type is PageInfoType
|| (type is IRuntimeTypeProvider { RuntimeType: { } runtimeType }
&& typeof(IPageInfo).IsAssignableFrom(runtimeType));
&& (typeof(IPageInfo).IsAssignableFrom(runtimeType)
|| runtimeType == typeof(PageCursor)));

public override void OnBeforeCompleteName(
ITypeCompletionContext completionContext,
Expand All @@ -156,20 +156,6 @@ public override void OnAfterCompleteName(
return;
}

// if we find a PagingInfo we will make all fields sharable.
if (configuration is ObjectTypeConfiguration typeCfg
&& typeCfg.Name.Equals(PageInfoType.Names.PageInfo))
{
foreach (var fieldCfg in typeCfg.Fields)
{
if (fieldCfg.Directives.All(t => t.Value is not ShareableDirective))
{
var typeRef = TypeReference.CreateDirective(_typeInspector.GetType(typeof(ShareableDirective)));
fieldCfg.Directives.Add(new DirectiveConfiguration(ShareableDirective.Default, typeRef));
}
}
}

var hasRuntimeType = (IRuntimeTypeProvider)configuration;
var type = hasRuntimeType.RuntimeType;

Expand Down Expand Up @@ -358,6 +344,8 @@ public override void OnBeforeCompleteType(
AddServiceTypeToQueryType(
completionContext,
configuration);

ApplyShareableToPagingType(configuration);
}

public override void OnAfterMakeExecutable(
Expand Down Expand Up @@ -497,6 +485,23 @@ private void AddServiceTypeToQueryType(
}
}

private void ApplyShareableToPagingType(TypeSystemConfiguration configuration)
{
// PageInfo and PageCursor are shared by every subgraph that exposes a connection,
// so they are marked @shareable at the type level.
if (_context.GetFederationVersion() == FederationVersion.Federation10
|| configuration is not ObjectTypeConfiguration typeCfg
|| typeCfg.Name is not (PageInfoType.Names.PageInfo or PageCursorTypeName)
|| typeCfg.Directives.Any(t => t.Value is ShareableDirective))
{
return;
}

var typeRef = TypeReference.CreateDirective(
_typeInspector.GetType(typeof(ShareableDirective)));
typeCfg.Directives.Add(new DirectiveConfiguration(ShareableDirective.Default, typeRef));
}

private void ApplyMethodLevelReferenceResolvers(
ObjectType objectType,
ObjectTypeConfiguration objectTypeCfg)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using HotChocolate.ApolloFederation.Types;
using HotChocolate.Execution;
using HotChocolate.Types;
using Microsoft.Extensions.DependencyInjection;

namespace HotChocolate.ApolloFederation;
Expand All @@ -19,4 +21,20 @@ public async Task PageConnection_Field_With_ApolloFederation_Builds_Schema()
// assert
schema.MatchSnapshot();
}

[Fact]
public async Task PageConnection_Field_With_Explicit_Shareable_PageCursor_Builds_Schema()
{
// arrange & act
var schema = await new ServiceCollection()
.AddGraphQL()
.AddApolloFederation()
.AddImplementationFirstTypes()
.AddPagingArguments()
.AddTypeExtension(new ObjectTypeExtension(d => d.Name("PageCursor").Shareable()))
.BuildSchemaAsync(cancellationToken: TestContext.Current.CancellationToken);

// assert
schema.MatchSnapshot();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,27 +50,27 @@ type EmployeeEdge {
}

"A cursor that points to a specific page."
type PageCursor {
type PageCursor @shareable {
"The page number."
page: Int!
"The cursor."
cursor: String!
}

"Information about pagination in a connection."
type PageInfo {
type PageInfo @shareable {
"Indicates whether more edges exist following the set defined by the clients arguments."
hasNextPage: Boolean! @shareable
hasNextPage: Boolean!
"Indicates whether more edges exist prior the set defined by the clients arguments."
hasPreviousPage: Boolean! @shareable
hasPreviousPage: Boolean!
"When paginating backwards, the cursor to continue."
startCursor: String @shareable
startCursor: String
"When paginating forwards, the cursor to continue."
endCursor: String @shareable
endCursor: String
"A list of cursors to continue paginating forwards."
forwardCursors: [PageCursor!]! @shareable
forwardCursors: [PageCursor!]!
"A list of cursors to continue paginating backwards."
backwardCursors: [PageCursor!]! @shareable
backwardCursors: [PageCursor!]!
}

"The _Any scalar is used to pass representations of entities from external services into the root _entities field for execution. Validation of the _Any scalar is done by matching the __typename and @external fields defined in the schema."
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
schema
@link(
url: "https://specs.apollo.dev/federation/v2.6"
import: ["@shareable", "@tag", "FieldSet"]
) {
query: Query
}

type Query {
employeeByPageTest(
"Returns the first _n_ elements from the list."
first: Int
"Returns the elements in the list that come after the specified cursor."
after: String
"Returns the last _n_ elements from the list."
last: Int
"Returns the elements in the list that come before the specified cursor."
before: String
): EmployeeConnection!
_service: _Service!
}

"This type provides a field named sdl: String! which exposes the SDL of the service's schema. This SDL (schema definition language) is a printed version of the service's schema including the annotations of federation directives. This SDL does not include the additions of the federation spec."
type _Service {
sdl: String!
}

type Employee {
empId: Int!
name: String!
}

"A connection to a list of items."
type EmployeeConnection {
"A list of edges."
edges: [EmployeeEdge!]
"A flattened list of the nodes"
nodes: [Employee!]
"Information to aid in pagination."
pageInfo: PageInfo!
"Identifies the total count of items in the connection."
totalCount: Int!
}

type EmployeeEdge {
"The item at the end of the edge."
node: Employee!
"A cursor for use in pagination."
cursor: String!
}

"A cursor that points to a specific page."
type PageCursor @shareable {
"The page number."
page: Int!
"The cursor."
cursor: String!
}

"Information about pagination in a connection."
type PageInfo @shareable {
"Indicates whether more edges exist following the set defined by the clients arguments."
hasNextPage: Boolean!
"Indicates whether more edges exist prior the set defined by the clients arguments."
hasPreviousPage: Boolean!
"When paginating backwards, the cursor to continue."
startCursor: String
"When paginating forwards, the cursor to continue."
endCursor: String
"A list of cursors to continue paginating forwards."
forwardCursors: [PageCursor!]!
"A list of cursors to continue paginating backwards."
backwardCursors: [PageCursor!]!
}

"The _Any scalar is used to pass representations of entities from external services into the root _entities field for execution. Validation of the _Any scalar is done by matching the __typename and @external fields defined in the schema."
scalar _Any

"Scalar representing a set of fields."
scalar FieldSet

"Links definitions within the document to external schemas."
directive @link(
"Gets imported specification url."
url: String!
"Gets optional list of imported element names."
import: [String!]
) repeatable on SCHEMA

"Indicates that given object and/or field can be resolved by multiple subgraphs."
directive @shareable repeatable on OBJECT | FIELD_DEFINITION
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,15 @@ type AddressesEdge {
}

"Information about pagination in a connection."
type PageInfo {
type PageInfo @shareable {
"Indicates whether more edges exist following the set defined by the clients arguments."
hasNextPage: Boolean! @shareable
hasNextPage: Boolean!
"Indicates whether more edges exist prior the set defined by the clients arguments."
hasPreviousPage: Boolean! @shareable
hasPreviousPage: Boolean!
"When paginating backwards, the cursor to continue."
startCursor: String @shareable
startCursor: String
"When paginating forwards, the cursor to continue."
endCursor: String @shareable
endCursor: String
}

"The _Any scalar is used to pass representations of entities from external services into the root _entities field for execution. Validation of the _Any scalar is done by matching the __typename and @external fields defined in the schema."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,15 @@ type AddressesEdge {
}

"Information about pagination in a connection."
type PageInfo {
type PageInfo @shareable {
"Indicates whether more edges exist following the set defined by the clients arguments."
hasNextPage: Boolean! @shareable
hasNextPage: Boolean!
"Indicates whether more edges exist prior the set defined by the clients arguments."
hasPreviousPage: Boolean! @shareable
hasPreviousPage: Boolean!
"When paginating backwards, the cursor to continue."
startCursor: String @shareable
startCursor: String
"When paginating forwards, the cursor to continue."
endCursor: String @shareable
endCursor: String
}

"The _Any scalar is used to pass representations of entities from external services into the root _entities field for execution. Validation of the _Any scalar is done by matching the __typename and @external fields defined in the schema."
Expand Down
Loading