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
1 change: 1 addition & 0 deletions src/All.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@
<Project Path="HotChocolate/Fusion/src/Fusion.SourceSchema.Packaging/HotChocolate.Fusion.SourceSchema.Packaging.csproj" />
</Folder>
<Folder Name="/HotChocolate/Fusion/test/">
<Project Path="HotChocolate/Fusion/test/Fusion.Aspire.Tests/HotChocolate.Fusion.Aspire.Tests.csproj" />
<Project Path="HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/HotChocolate.Fusion.AspNetCore.Tests.csproj" />
<Project Path="HotChocolate/Fusion/test/Fusion.Composition.Tests/HotChocolate.Fusion.Composition.Tests.csproj" />
<Project Path="HotChocolate/Fusion/test/Fusion.Composition.ApolloFederation.Tests/HotChocolate.Fusion.Composition.ApolloFederation.Tests.csproj" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,13 @@ public interface ISelectionSet
bool HasIncrementalParts { get; }

/// <summary>
/// Gets the type that declares this selection set.
/// Gets the complex type that declares this selection set.
/// </summary>
IObjectTypeDefinition Type { get; }
/// <remarks>
/// This is typically an object type. It can be an interface type when the concrete runtime
/// type of a selection set is not yet known.
/// </remarks>
IComplexTypeDefinition Type { get; }

/// <summary>
/// Gets the selections that shall be executed.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ internal SelectionSet(
/// </summary>
public IObjectTypeDefinition Type { get; }

IComplexTypeDefinition ISelectionSet.Type => Type;

/// <summary>
/// Gets the declaring operation.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
namespace HotChocolate.Types.Composite;

/// <summary>
/// <para>
/// The @external directive indicates that a field is recognized by the current source schema but is
/// not directly contributed (resolved) by it. Instead, the source schema references the field for
/// specific composition purposes, for example as part of a @requires selection.
/// </para>
/// <para>
/// <see href="https://graphql.github.io/composite-schemas-spec/draft/#sec--external"/>
/// </para>
/// <code>
/// type User @key(fields: "id") {
/// id: ID!
/// name: String! @external
/// username: String! @requires(fields: "name")
/// }
///
/// directive @external on FIELD_DEFINITION
/// </code>
/// </summary>
[DirectiveType(
DirectiveNames.External.Name,
DirectiveLocation.FieldDefinition,
IsRepeatable = false)]
public sealed class External
{
private External()
{
}

/// <inheritdoc />
public override string ToString() => "@external";

/// <summary>
/// The singleton instance of the <see cref="External"/> directive.
/// </summary>
public static External Instance { get; } = new();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
namespace HotChocolate.Types.Composite;

/// <summary>
/// <para>
/// The @implement directive marks a field as an explicit implementation that intentionally replaces
/// a default field implementation contributed to an interface by an @interfaceObject stand-in. It is
/// required on an implementing type's field, or on a more specific interface's own stand-in field,
/// that collides with an applicable default.
/// </para>
/// <para>
/// <see href="https://graphql.github.io/composite-schemas-spec/draft/#sec--implement"/>
/// </para>
/// <code>
/// type Chair implements Product {
/// id: ID!
/// taxRate: Float! @implement
/// }
///
/// directive @implement on FIELD_DEFINITION
/// </code>
/// </summary>
[DirectiveType(
DirectiveNames.Implement.Name,
DirectiveLocation.FieldDefinition,
IsRepeatable = false)]
public sealed class Implement
{
private Implement()
{
}

/// <inheritdoc />
public override string ToString() => "@implement";

/// <summary>
/// The singleton instance of the <see cref="Implement"/> directive.
/// </summary>
public static Implement Instance { get; } = new();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
namespace HotChocolate.Types.Composite;

/// <summary>
/// <para>
/// The @interfaceObject directive is used within a source schema to declare an object type that
/// acts as a stand-in for an interface defined in another source schema. The stand-in carries the
/// same name as the interface and allows a source schema to contribute fields to that interface
/// without defining any of its implementing types.
/// </para>
/// <para>
/// <see href="https://graphql.github.io/composite-schemas-spec/draft/#sec--interfaceObject"/>
/// </para>
/// <code>
/// type Media @interfaceObject @key(fields: "id") {
/// id: ID!
/// reviews: [Review!]!
/// }
///
/// directive @interfaceObject on OBJECT
/// </code>
/// </summary>
[DirectiveType(
DirectiveNames.InterfaceObject.Name,
DirectiveLocation.Object,
IsRepeatable = false)]
public sealed class InterfaceObject
{
private InterfaceObject()
{
}

/// <inheritdoc />
public override string ToString() => "@interfaceObject";

/// <summary>
/// The singleton instance of the <see cref="InterfaceObject"/> directive.
/// </summary>
public static InterfaceObject Instance { get; } = new();
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,33 @@ public void Prepare_One_Field()
MatchSnapshot(document, operation);
}

[Fact]
public void Compile_Should_ExposeObjectSelectionSetThroughPublicContract()
{
// arrange
var schema = SchemaBuilder.New()
.AddQueryType(
c => c
.Name("Query")
.Field("foo")
.Type<StringType>()
.Resolve("foo"))
.Create();
var document = Utf8GraphQLParser.Parse("{ foo }");

// act
var operation = OperationCompiler.Compile("opid", document, schema);
ISelectionSet selectionSet = operation.RootSelectionSet;

// assert
Assert.Equal("Query", selectionSet.Type.Name);
Assert.IsAssignableFrom<IObjectTypeDefinition>(selectionSet.Type);

var selection = Assert.Single(selectionSet.GetSelections());
Assert.Equal("Query", selection.DeclaringSelectionSet.Type.Name);
Assert.IsAssignableFrom<IObjectTypeDefinition>(selection.DeclaringSelectionSet.Type);
}

[Fact]
public void Prepare_Duplicate_Field()
{
Expand Down
1 change: 1 addition & 0 deletions src/HotChocolate/Fusion/HotChocolate.Fusion.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
<Project Path="src/Fusion.Utilities/HotChocolate.Fusion.Utilities.csproj" />
</Folder>
<Folder Name="/test/">
<Project Path="test/Fusion.Aspire.Tests/HotChocolate.Fusion.Aspire.Tests.csproj" />
<Project Path="test/Fusion.AspNetCore.Tests/HotChocolate.Fusion.AspNetCore.Tests.csproj" />
<Project Path="test/Fusion.Composition.Tests/HotChocolate.Fusion.Composition.Tests.csproj" />
<Project Path="test/Fusion.Composition.ApolloFederation.Tests/HotChocolate.Fusion.Composition.ApolloFederation.Tests.csproj" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,7 @@ public static async Task<bool> TryComposeAsync(

var compositionLog = new CompositionLog();
var environment = settings.EnvironmentName ?? "Aspire";
var compositionSettings = new CompositionSettings
{
Merger = { EnableGlobalObjectIdentification = settings.EnableGlobalObjectIdentification },
Satisfiability = { IncludeSatisfiabilityPaths = settings.IncludeSatisfiabilityPaths },
Preprocessor = { ExcludeByTag = settings.ExcludeByTag?.ToHashSet() }
};
var compositionSettings = CreateCompositionSettings(settings);
var sourceSchemas = newSourceSchemas.ToDictionary(
s => s.Name,
s => (s.Schema, s.SchemaSettings));
Expand Down Expand Up @@ -67,6 +62,26 @@ public static async Task<bool> TryComposeAsync(
return true;
}

internal static CompositionSettings CreateCompositionSettings(
GraphQLCompositionSettings settings)
{
return new CompositionSettings
{
Merger =
{
EnableGlobalObjectIdentification = settings.EnableGlobalObjectIdentification,
NodeResolution = settings.NodeResolution
},
Satisfiability = { IncludeSatisfiabilityPaths = settings.IncludeSatisfiabilityPaths },
ApolloFederationCompatibility =
{
AllowNonResolvableInterfaceObjects = settings.AllowNonResolvableInterfaceObjects,
ShareableFieldRuntimeTypeRouting = settings.ShareableFieldRuntimeTypeRouting
},
Preprocessor = { ExcludeByTag = settings.ExcludeByTag?.ToHashSet() }
};
}

/// <summary>
/// Since we're prefixing the message with an emoji and space before printing,
/// we need to also indent each line of a multiline message by three spaces to fix the alignment.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,29 @@ public struct GraphQLCompositionSettings
/// </summary>
public bool? EnableGlobalObjectIdentification { get; set; }

/// <summary>
/// Gets or sets how the gateway resolves the <c>Query.node</c> field.
/// </summary>
public NodeResolution? NodeResolution { get; set; }

/// <summary>
/// Gets or sets a value indicating whether satisfiability paths should be included in the
/// composed schema.
/// </summary>
public bool? IncludeSatisfiabilityPaths { get; set; }

/// <summary>
/// Gets or sets whether Apollo Federation non-resolvable interface objects are accepted and
/// unresolved projected fields are reported as field errors at runtime.
/// </summary>
public bool? AllowNonResolvableInterfaceObjects { get; set; }

/// <summary>
/// Gets or sets how runtime types are routed for Apollo Federation shareable fields whose
/// result type is abstract.
/// </summary>
public ShareableFieldRuntimeTypeRouting? ShareableFieldRuntimeTypeRouting { get; set; }

/// <summary>
/// Gets or sets the set of tags whose annotated schema elements shall be excluded from
/// composition.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
<IsAotCompatible>false</IsAotCompatible>
</PropertyGroup>

<ItemGroup>
<InternalsVisibleTo Include="HotChocolate.Fusion.Aspire.Tests" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Aspire.Hosting" />
<PackageReference Include="MessagePack" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace HotChocolate.Fusion.ApolloFederation;

/// <summary>
/// Processing-only metadata that records enabled Apollo Federation compatibility
/// capabilities on a source schema while it is composed.
/// </summary>
internal sealed class ApolloFederationCompatibilityMetadata
{
public bool AllowNonResolvableInterfaceObjects { get; init; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ internal static class FederationSchemaAnalyzer
FederationDirectiveNames.ComposeDirective,
FederationDirectiveNames.Authenticated,
FederationDirectiveNames.RequiresScopes,
FederationDirectiveNames.Policy,
FederationDirectiveNames.InterfaceObject
FederationDirectiveNames.Policy
];

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,14 @@ public static CompositionResult<string> Transform(string federationSdl)
.ToImmutableArray();
}

SourceExternalFieldMetadata.Capture(schema);
RemoveFederationInfrastructure.Apply(schema);
GenerateLookupFields.Apply(schema);
RewriteKeyDirectives.Apply(schema);
TransformRequiresToRequire.Apply(schema);
RemoveExternalFields.Apply(schema);
GenerateNodeLookup.Apply(schema);
SourceExternalFieldMetadata.WriteMarker(schema);

return SchemaFormatter.FormatAsString(schema);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,17 @@ public static void Apply(

foreach (var type in schema.Types)
{
if (type is not MutableComplexTypeDefinition complexType)
// '@shareable' is only meaningful on object type fields. Interface types can carry
// a '@key' (entity interfaces), but their field definitions must never be stamped
// '@shareable' or the composition's shareable-usage validation rejects them.
if (type is not MutableObjectTypeDefinition objectType)
{
continue;
}

foreach (var field in complexType.Fields)
foreach (var field in objectType.Fields)
{
if (!keyReferences.Contains((complexType.Name, field.Name)))
if (!keyReferences.Contains((objectType.Name, field.Name)))
{
continue;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,11 @@ public static void Apply(MutableSchemaDefinition schema)
continue;
}

if (keyReferences.Contains((complexType.Name, field.Name)))
if (keyReferences.Contains((complexType.Name, field.Name))
|| SourceExternalFieldMetadata.Contains(
schema,
complexType.Name,
field.Name))
{
var externalDirective = field.Directives.FirstOrDefault(
FederationDirectiveNames.External);
Expand Down Expand Up @@ -375,7 +379,7 @@ private static bool IsReferencedByOutputField(
return referenced;
}

private static HashSet<(string TypeName, string FieldName)> CollectKeyReferences(
internal static HashSet<(string TypeName, string FieldName)> CollectKeyReferences(
MutableSchemaDefinition schema)
{
var referenced = new HashSet<(string, string)>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ namespace HotChocolate.Fusion.ApolloFederation;
/// </summary>
internal static class RemoveFederationInfrastructure
{
private static readonly HashSet<string> s_federationDirectiveNames = new(StringComparer.Ordinal)
{
private static readonly HashSet<string> s_federationDirectiveNames =
[
with(StringComparer.Ordinal),
FederationDirectiveNames.Key,
FederationDirectiveNames.Requires,
FederationDirectiveNames.Provides,
Expand All @@ -20,19 +21,19 @@ internal static class RemoveFederationInfrastructure
FederationDirectiveNames.Inaccessible,
FederationDirectiveNames.Override,
FederationDirectiveNames.Tag,
FederationDirectiveNames.InterfaceObject,
FederationDirectiveNames.ComposeDirective,
FederationDirectiveNames.Authenticated,
FederationDirectiveNames.RequiresScopes,
FederationDirectiveNames.Policy
};
];

private static readonly HashSet<string> s_federationScalarNames = new(StringComparer.Ordinal)
{
private static readonly HashSet<string> s_federationScalarNames =
[
with(StringComparer.Ordinal),
FederationTypeNames.Any,
FederationTypeNames.FieldSet,
FederationTypeNames.LegacyFieldSet
};
];

/// <summary>
/// Applies the transformation to remove federation infrastructure from the schema.
Expand Down
Loading
Loading