Skip to content
This repository has been archived by the owner on Aug 30, 2024. It is now read-only.

Commit

Permalink
fix: ability to configure interface entities (#7)
Browse files Browse the repository at this point in the history
Adds new `KeyInterfaceAttribute` that allows specifying `@key` directive
on GraphQL interfaces.

Resolves:
#5
  • Loading branch information
dariuszkuc authored Oct 12, 2023
1 parent b8c82e6 commit 7b30f95
Show file tree
Hide file tree
Showing 3 changed files with 169 additions and 0 deletions.
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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<string> 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).
Expand Down
38 changes: 38 additions & 0 deletions src/Federation/Extensions/ApolloFederationDescriptorExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,44 @@ public static IEntityResolverDescriptor Key(
return new EntityResolverDescriptor<object>(descriptor);
}

/// <inheritdoc cref="Key(IObjectTypeDescriptor)"/>
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<ArgumentNode> arguments = new List<ArgumentNode> {
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());
}

/// <summary>
/// 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
Expand Down
85 changes: 85 additions & 0 deletions src/Federation/KeyInterfaceAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
using HotChocolate.Types.Descriptors;
using static ApolloGraphQL.HotChocolate.Federation.ThrowHelper;

namespace ApolloGraphQL.HotChocolate.Federation;

/// <summary>
/// <code>
/// # 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
/// </code>
///
/// 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.
/// <example>
/// type Foo @key(fields: "id") {
/// id: ID!
/// field: String
/// bars: [Bar!]!
/// }
///
/// type Bar @key(fields: "id", resolvable: false) {
/// id: ID!
/// }
/// </example>
/// </summary>
public sealed class KeyInterfaceAttribute : InterfaceTypeDescriptorAttribute
{
/// <summary>
/// Initializes a new instance of <see cref="KeyInterfaceAttribute"/>.
/// </summary>
/// <param name="fieldSet">
/// The field set that describes the key.
/// Grammatically, a field set is a selection set minus the braces.
/// </param>
public KeyInterfaceAttribute(string fieldSet)
{
FieldSet = fieldSet;
Resolvable = null;
}

/// <summary>
/// Initializes a new instance of <see cref="KeyInterfaceAttribute"/>.
/// </summary>
/// <param name="fieldSet">
/// The field set that describes the key.
/// Grammatically, a field set is a selection set minus the braces.
/// </param>
/// <param name="resolvable">
/// Boolean flag to indicate whether this entity is resolvable locally.
/// </param>
public KeyInterfaceAttribute(string fieldSet, bool? resolvable = null)
{
FieldSet = fieldSet;
Resolvable = resolvable;
}

/// <summary>
/// Gets the field set that describes the key.
/// Grammatically, a field set is a selection set minus the braces.
/// </summary>
public string FieldSet { get; }

/// <summary>
/// Gets the resolvable flag.
/// </summary>
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);
}
}

0 comments on commit 7b30f95

Please sign in to comment.