diff --git a/README.md b/README.md index 31d1190..856be3e 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,7 @@ Federation v2 directives (includes all of the v1 directives) * `ComposeDirective` applicable on schema, see [`@composeDirective` documentation](https://www.apollographql.com/docs/federation/federated-types/federated-directives#composedirective) * `Inaccessible` applicable on all type definitions, see [`@inaccessible` documentation](https://www.apollographql.com/docs/federation/federated-types/federated-directives#inaccessible) * `InterfaceObject` applicable on objects, see [`@interfaceObject` documentation](https://www.apollographql.com/docs/federation/federated-types/federated-directives#interfaceobject) +* `KeyInterface` applicable on interfaces, see [entity interface `@key` documentation](https://www.apollographql.com/docs/federation/federated-types/interfaces) * `Link` applicable on schema, see [`@link` documentation](https://www.apollographql.com/docs/federation/federated-types/federated-directives#the-link-directive) * `Shareable` applicable on schema, see [`@shareable` documentation](https://www.apollographql.com/docs/federation/federated-types/federated-directives#shareable) @@ -248,6 +249,51 @@ app.MapGraphQL(); app.Run(); ``` +#### `@interfaceObject` usage + +Apollo Federation v2 supports **entity interfaces**, a powerful extension to the GraphQL interfaces that allows you to extend functionality of an interface across the supergraph +without having to implement (or even be aware of) all its implementing types. + +In a subgraph defininig the interface we need to apply `@key` + +```csharp +[InterfaceType] +[KeyInterface("id")] +public interface Product +{ + [ID] + string Id { get; } + + string Name { get; } +} + +[Key("id")] +public class Book : Product +{ + [ID] + public string Id { get; set; } + + public string Name { get; set; } + + public string Content { get; set; } +} +``` + +We can then extend the interface in another subgraph by making it a type, applying `@interfaceObject` and same `@key` directive. This allows you add new fields to every +entity that implements your interface (e.g. adding `Reviews` field to all `Product` implementations). + +```csharp +[Key("id")] +[InterfaceObject] +public class Product +{ + [ID] + public string Id { get; set; } + + public List Reviews { get; set; } +} +``` + ## Contact If you have a specific question about the library or code, please start a discussion in the [Apollo community forums](https://community.apollographql.com/) or start a conversation on our [Discord server](https://discord.gg/graphos). diff --git a/src/Federation/Extensions/ApolloFederationDescriptorExtensions.cs b/src/Federation/Extensions/ApolloFederationDescriptorExtensions.cs index 6b81fe2..5473e8f 100644 --- a/src/Federation/Extensions/ApolloFederationDescriptorExtensions.cs +++ b/src/Federation/Extensions/ApolloFederationDescriptorExtensions.cs @@ -266,6 +266,44 @@ public static IEntityResolverDescriptor Key( return new EntityResolverDescriptor(descriptor); } + /// + public static IInterfaceTypeDescriptor Key( + this IInterfaceTypeDescriptor descriptor, + string fieldSet, + bool? resolvable = null) + { + if (descriptor is null) + { + throw new ArgumentNullException(nameof(descriptor)); + } + + if (string.IsNullOrEmpty(fieldSet)) + { + throw new ArgumentException( + FieldDescriptorExtensions_Key_FieldSet_CannotBeNullOrEmpty, + nameof(fieldSet)); + } + + List arguments = new List { + new ArgumentNode( + WellKnownArgumentNames.Fields, + new StringValueNode(fieldSet) + ) + }; + if (false == resolvable) + { + arguments.Add( + new ArgumentNode( + WellKnownArgumentNames.Resolvable, + new BooleanValueNode(false) + ) + ); + } + return descriptor.Directive( + WellKnownTypeNames.Key, + arguments.ToArray()); + } + /// /// Applies @link directive definitions to link the document to external schemas. /// External schemas are identified by their url, which optionally ends with a name and version with diff --git a/src/Federation/KeyInterfaceAttribute.cs b/src/Federation/KeyInterfaceAttribute.cs new file mode 100644 index 0000000..b8101ae --- /dev/null +++ b/src/Federation/KeyInterfaceAttribute.cs @@ -0,0 +1,85 @@ +using HotChocolate.Types.Descriptors; +using static ApolloGraphQL.HotChocolate.Federation.ThrowHelper; + +namespace ApolloGraphQL.HotChocolate.Federation; + +/// +/// +/// # federation v1 definition +/// directive @key(fields: _FieldSet!) repeatable on OBJECT | INTERFACE +/// +/// # federation v2 definition +/// directive @key(fields: FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE +/// +/// +/// The @key directive is used to indicate a combination of fields that can be used to uniquely +/// identify and fetch an object or interface. The specified field set can represent single field (e.g. "id"), +/// multiple fields (e.g. "id name") or nested selection sets (e.g. "id user { name }"). Multiple keys can +/// be specified on a target type. +/// +/// Keys can also be marked as non-resolvable which indicates to router that given entity should never be +/// resolved within given subgraph. This allows your subgraph to still reference target entity without +/// contributing any fields to it. +/// +/// type Foo @key(fields: "id") { +/// id: ID! +/// field: String +/// bars: [Bar!]! +/// } +/// +/// type Bar @key(fields: "id", resolvable: false) { +/// id: ID! +/// } +/// +/// +public sealed class KeyInterfaceAttribute : InterfaceTypeDescriptorAttribute +{ + /// + /// Initializes a new instance of . + /// + /// + /// The field set that describes the key. + /// Grammatically, a field set is a selection set minus the braces. + /// + public KeyInterfaceAttribute(string fieldSet) + { + FieldSet = fieldSet; + Resolvable = null; + } + + /// + /// Initializes a new instance of . + /// + /// + /// The field set that describes the key. + /// Grammatically, a field set is a selection set minus the braces. + /// + /// + /// Boolean flag to indicate whether this entity is resolvable locally. + /// + public KeyInterfaceAttribute(string fieldSet, bool? resolvable = null) + { + FieldSet = fieldSet; + Resolvable = resolvable; + } + + /// + /// Gets the field set that describes the key. + /// Grammatically, a field set is a selection set minus the braces. + /// + public string FieldSet { get; } + + /// + /// Gets the resolvable flag. + /// + public bool? Resolvable { get; } + + protected override void OnConfigure(IDescriptorContext context, IInterfaceTypeDescriptor descriptor, Type type) + { + if (string.IsNullOrEmpty(FieldSet)) + { + throw Key_FieldSet_CannotBeEmpty(type); + } + descriptor.Key(FieldSet, Resolvable); + } +}