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
5 changes: 5 additions & 0 deletions src/All.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -214,9 +214,11 @@
<Project Path="HotChocolate/Fusion/src/Fusion.Aspire/HotChocolate.Fusion.Aspire.csproj" />
<Project Path="HotChocolate/Fusion/src/Fusion.AspNetCore/HotChocolate.Fusion.AspNetCore.csproj" />
<Project Path="HotChocolate/Fusion/src/Fusion.Composition/HotChocolate.Fusion.Composition.csproj" />
<Project Path="HotChocolate/Fusion/src/Fusion.Composition.ApolloFederation/HotChocolate.Fusion.Composition.ApolloFederation.csproj" />
<Project Path="HotChocolate/Fusion/src/Fusion.Caching/HotChocolate.Fusion.Caching.csproj" />
<Project Path="HotChocolate/Fusion/src/Fusion.Diagnostics/HotChocolate.Fusion.Diagnostics.csproj" />
<Project Path="HotChocolate/Fusion/src/Fusion.Execution.Types/HotChocolate.Fusion.Execution.Types.csproj" />
<Project Path="HotChocolate/Fusion/src/Fusion.Connectors.ApolloFederation/HotChocolate.Fusion.Connectors.ApolloFederation.csproj" />
<Project Path="HotChocolate/Fusion/src/Fusion.Connectors.InMemory/HotChocolate.Fusion.Connectors.InMemory.csproj" />
<Project Path="HotChocolate/Fusion/src/Fusion.Execution/HotChocolate.Fusion.Execution.csproj" />
<Project Path="HotChocolate/Fusion/src/Fusion.Language/HotChocolate.Fusion.Language.csproj" />
Expand All @@ -227,6 +229,9 @@
<Folder Name="/HotChocolate/Fusion/test/">
<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" />
<Project Path="HotChocolate/Fusion/test/Fusion.Connectors.ApolloFederation.Compliance.Tests/HotChocolate.Fusion.Connectors.ApolloFederation.Compliance.Tests.csproj" />
<Project Path="HotChocolate/Fusion/test/Fusion.Connectors.ApolloFederation.Tests/HotChocolate.Fusion.Connectors.ApolloFederation.Tests.csproj" />
<Project Path="HotChocolate/Fusion/test/Fusion.Connectors.InMemory.Tests/HotChocolate.Fusion.Connectors.InMemory.Tests.csproj" />
<Project Path="HotChocolate/Fusion/test/Fusion.Caching.Tests/HotChocolate.Fusion.Caching.Tests.csproj" />
<Project Path="HotChocolate/Fusion/test/Fusion.Diagnostics.Tests/HotChocolate.Fusion.Diagnostics.Tests.csproj" />
Expand Down
5 changes: 5 additions & 0 deletions src/HotChocolate/Fusion/HotChocolate.Fusion.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
<Project Path="src/Fusion.Aspire/HotChocolate.Fusion.Aspire.csproj" />
<Project Path="src/Fusion.AspNetCore/HotChocolate.Fusion.AspNetCore.csproj" />
<Project Path="src/Fusion.Composition/HotChocolate.Fusion.Composition.csproj" />
<Project Path="src/Fusion.Composition.ApolloFederation/HotChocolate.Fusion.Composition.ApolloFederation.csproj" />
<Project Path="src/Fusion.Connectors.ApolloFederation/HotChocolate.Fusion.Connectors.ApolloFederation.csproj" />
<Project Path="src/Fusion.Caching/HotChocolate.Fusion.Caching.csproj" />
<Project Path="src/Fusion.Diagnostics/HotChocolate.Fusion.Diagnostics.csproj" />
<Project Path="src/Fusion.Execution.Types/HotChocolate.Fusion.Execution.Types.csproj" />
Expand All @@ -19,6 +21,9 @@
<Folder Name="/test/">
<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" />
<Project Path="test/Fusion.Connectors.ApolloFederation.Compliance.Tests/HotChocolate.Fusion.Connectors.ApolloFederation.Compliance.Tests.csproj" />
<Project Path="test/Fusion.Connectors.ApolloFederation.Tests/HotChocolate.Fusion.Connectors.ApolloFederation.Tests.csproj" />
<Project Path="test/Fusion.Caching.Tests/HotChocolate.Fusion.Caching.Tests.csproj" />
<Project Path="test/Fusion.Diagnostics.Tests/HotChocolate.Fusion.Diagnostics.Tests.csproj" />
<Project Path="test/Fusion.EventSources.Tests/HotChocolate.Fusion.EventSources.Tests.csproj" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace HotChocolate.Fusion.ApolloFederation;

/// <summary>
/// Represents a single <c>@key</c> directive on a federation entity type.
/// </summary>
internal sealed class EntityKeyInfo
{
/// <summary>
/// Gets the raw field selection string from <c>@key(fields: "...")</c>.
/// </summary>
public required string Fields { get; init; }

/// <summary>
/// Gets a value indicating whether the key is resolvable.
/// Defaults to <c>true</c> when the <c>resolvable</c> argument is omitted.
/// </summary>
public bool Resolvable { get; init; } = true;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace HotChocolate.Fusion.ApolloFederation;

internal static class FederationDirectiveNames
{
public const string Key = "key";
public const string Requires = "requires";
public const string Provides = "provides";
public const string External = "external";
public const string Link = "link";
public const string Shareable = "shareable";
public const string Inaccessible = "inaccessible";
public const string Override = "override";
public const string Tag = "tag";
public const string InterfaceObject = "interfaceObject";
public const string ComposeDirective = "composeDirective";
public const string Authenticated = "authenticated";
public const string RequiresScopes = "requiresScopes";
public const string Policy = "policy";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace HotChocolate.Fusion.ApolloFederation;

internal static class FederationFieldNames
{
public const string Entities = "_entities";
public const string Service = "_service";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
using HotChocolate.Fusion.Errors;
using HotChocolate.Language;
using HotChocolate.Types.Mutable;
using static HotChocolate.Fusion.ApolloFederation.Properties.FederationResources;

namespace HotChocolate.Fusion.ApolloFederation;

/// <summary>
/// Validates a <see cref="MutableSchemaDefinition"/> for Apollo Federation v2 compatibility
/// and detects unsupported directives.
/// </summary>
internal static class FederationSchemaAnalyzer
{
private const string FederationUrlPrefix = "specs.apollo.dev/federation";

private static readonly HashSet<string> s_unsupportedDirectives =
[
FederationDirectiveNames.ComposeDirective,
FederationDirectiveNames.Authenticated,
FederationDirectiveNames.RequiresScopes,
FederationDirectiveNames.Policy,
FederationDirectiveNames.InterfaceObject
];

/// <summary>
/// Validates the given federation schema and returns any composition errors.
/// </summary>
/// <param name="schema">
/// The mutable schema definition to validate.
/// </param>
/// <returns>
/// A list of <see cref="CompositionError"/> instances. An empty list indicates success.
/// </returns>
public static List<CompositionError> Validate(MutableSchemaDefinition schema)
{
var errors = new List<CompositionError>();

ValidateFederationVersion(schema, errors);
ValidateUnsupportedDirectives(schema, errors);

return errors;
}

private static void ValidateFederationVersion(
MutableSchemaDefinition schema,
List<CompositionError> errors)
{
var federationVersion = FindFederationVersion(schema);

if (federationVersion is null)
{
errors.Add(new CompositionError(FederationSchemaAnalyzer_FederationV1NotSupported));
}
}

private static string? FindFederationVersion(MutableSchemaDefinition schema)
{
foreach (var directive in schema.Directives)
{
if (!directive.Name.Equals(FederationDirectiveNames.Link, StringComparison.Ordinal))
{
continue;
}

if (!directive.Arguments.TryGetValue("url", out var urlValue)
|| urlValue is not StringValueNode urlString)
{
continue;
}

var url = urlString.Value;

if (!url.Contains(FederationUrlPrefix, StringComparison.Ordinal))
{
continue;
}

// Extract version from URL like
// "https://specs.apollo.dev/federation/v2.5"
var lastSlash = url.LastIndexOf('/');

if (lastSlash >= 0 && lastSlash < url.Length - 1)
{
return url[(lastSlash + 1)..];
}
}

return null;
}

private static void ValidateUnsupportedDirectives(
MutableSchemaDefinition schema,
List<CompositionError> errors)
{
foreach (var name in s_unsupportedDirectives)
{
if (schema.DirectiveDefinitions.ContainsName(name))
{
errors.Add(new CompositionError(string.Format(
FederationSchemaAnalyzer_DirectiveNotSupported,
name)));
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System.Collections.Immutable;
using HotChocolate.Fusion.Errors;
using HotChocolate.Fusion.Results;
using HotChocolate.Language;
using HotChocolate.Types.Mutable;
using HotChocolate.Types.Mutable.Serialization;
using static HotChocolate.Fusion.ApolloFederation.Properties.FederationResources;

namespace HotChocolate.Fusion.ApolloFederation;

/// <summary>
/// Transforms an Apollo Federation v2 subgraph SDL into a Composite Schema Spec
/// source schema SDL suitable for the HotChocolate Fusion composition pipeline.
/// </summary>
public static class FederationSchemaTransformer
{
/// <summary>
/// Transforms the given Apollo Federation v2 subgraph SDL.
/// </summary>
/// <param name="federationSdl">
/// The Apollo Federation v2 subgraph SDL to transform.
/// </param>
/// <returns>
/// A <see cref="CompositionResult{TValue}"/> containing the transformed SDL string
/// on success, or composition errors on failure.
/// </returns>
public static CompositionResult<string> Transform(string federationSdl)
{
ArgumentException.ThrowIfNullOrEmpty(federationSdl);

MutableSchemaDefinition schema;

try
{
schema = SchemaParser.Parse(federationSdl);
}
catch (SyntaxException ex)
{
return new CompositionError(
string.Format(FederationSchemaTransformer_ParseFailed, ex.Message));
}

var errors = FederationSchemaAnalyzer.Validate(schema);

if (errors.Count > 0)
{
return errors.ToImmutableArray();
}

RemoveFederationInfrastructure.Apply(schema);
GenerateLookupFields.Apply(schema);
RewriteKeyDirectives.Apply(schema);
TransformRequiresToRequire.Apply(schema);
RemoveExternalFields.Apply(schema);

return SchemaFormatter.FormatAsString(schema);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace HotChocolate.Fusion.ApolloFederation;

internal static class FederationTypeNames
{
public const string Any = "_Any";
public const string Entity = "_Entity";
public const string Service = "_Service";
public const string FieldSet = "FieldSet";
public const string LegacyFieldSet = "_FieldSet";
}
Loading
Loading