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
Expand Up @@ -6,6 +6,7 @@ internal static class FederationDirectiveNames
public const string Requires = "requires";
public const string Provides = "provides";
public const string External = "external";
public const string Extends = "extends";
public const string Link = "link";
public const string Shareable = "shareable";
public const string Inaccessible = "inaccessible";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ public static CompositionResult<string> Transform(string federationSdl)
RemoveExternalFields.Apply(schema);
GenerateNodeLookup.Apply(schema);
SourceExternalFieldMetadata.WriteMarker(schema);
RemoveEmptyQueryRoot.Apply(schema);

return SchemaFormatter.FormatAsString(schema);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using HotChocolate.Fusion.Definitions;
using HotChocolate.Types;
using HotChocolate.Types.Mutable;

namespace HotChocolate.Fusion.ApolloFederation;

internal static class FederationV1DirectiveDefinitions
{
public static void Apply(MutableSchemaDefinition schema)
{
if (!schema.Types.TryGetType<MutableScalarTypeDefinition>(
WellKnownTypeNames.FieldSelectionSet,
out var fieldSelectionSetType))
{
return;
}

Replace(schema, CreateKey(fieldSelectionSetType));
Replace(schema, new RequiresMutableDirectiveDefinition(fieldSelectionSetType));
Replace(schema, CreateExternal());
Replace(schema, CreateExtends());
}

private static MutableDirectiveDefinition CreateKey(
MutableScalarTypeDefinition fieldSelectionSetType)
{
var definition = new MutableDirectiveDefinition(FederationDirectiveNames.Key)
{
IsRepeatable = true,
Locations = DirectiveLocation.Object | DirectiveLocation.Interface
};

definition.Arguments.Add(
new MutableInputFieldDefinition(
WellKnownArgumentNames.Fields,
new NonNullType(fieldSelectionSetType)));
return definition;
}

private static MutableDirectiveDefinition CreateExternal()
=> new(FederationDirectiveNames.External)
{
Locations = DirectiveLocation.FieldDefinition
};

private static MutableDirectiveDefinition CreateExtends()
=> new(FederationDirectiveNames.Extends)
{
Locations = DirectiveLocation.Object | DirectiveLocation.Interface
};

private static void Replace(
MutableSchemaDefinition schema,
MutableDirectiveDefinition definition)
{
schema.DirectiveDefinitions.Remove(definition.Name);
schema.DirectiveDefinitions.Add(definition);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
using HotChocolate.Fusion.Logging;
using HotChocolate.Fusion.Logging.Contracts;
using HotChocolate.Types;
using HotChocolate.Types.Mutable;
using static HotChocolate.Fusion.ApolloFederation.Properties.FederationResources;

namespace HotChocolate.Fusion.ApolloFederation;

internal static class FederationV1SchemaAnalyzer
{
private static readonly Dictionary<string, DirectiveSignature> s_signatures = new(
StringComparer.Ordinal)
{
[FederationDirectiveNames.Key] = new(
DirectiveLocation.Object | DirectiveLocation.Interface,
WellKnownArgumentNames.Fields,
"the required 'fields' argument only"),
[FederationDirectiveNames.Extends] = new(
DirectiveLocation.Object | DirectiveLocation.Interface,
null,
"no arguments"),
[FederationDirectiveNames.External] = new(
DirectiveLocation.FieldDefinition,
null,
"no arguments"),
[FederationDirectiveNames.Requires] = new(
DirectiveLocation.FieldDefinition,
WellKnownArgumentNames.Fields,
"the required 'fields' argument only"),
[FederationDirectiveNames.Provides] = new(
DirectiveLocation.FieldDefinition,
WellKnownArgumentNames.Fields,
"the required 'fields' argument only"),
[FederationDirectiveNames.Tag] = new(
DirectiveLocation.FieldDefinition
| DirectiveLocation.Object
| DirectiveLocation.Interface
| DirectiveLocation.Union
| DirectiveLocation.ArgumentDefinition
| DirectiveLocation.Scalar
| DirectiveLocation.Enum
| DirectiveLocation.EnumValue
| DirectiveLocation.InputObject
| DirectiveLocation.InputFieldDefinition,
"name",
"the required 'name' argument only")
};

public static void Validate(
MutableSchemaDefinition schema,
ICompositionLog log)
{
ValidateProvider(schema, schema, DirectiveLocation.Schema, "schema", log);

foreach (var type in schema.Types)
{
ValidateProvider(schema, type, GetTypeLocation(type), type.Name, log);

switch (type)
{
case MutableComplexTypeDefinition complexType:
foreach (var field in complexType.Fields)
{
ValidateProvider(
schema,
field,
DirectiveLocation.FieldDefinition,
field.Coordinate.ToString(),
log);

foreach (var argument in field.Arguments)
{
ValidateProvider(
schema,
argument,
DirectiveLocation.ArgumentDefinition,
argument.Coordinate.ToString(),
log);
}
}

break;

case MutableInputObjectTypeDefinition inputObjectType:
foreach (var field in inputObjectType.Fields)
{
ValidateProvider(
schema,
field,
DirectiveLocation.InputFieldDefinition,
field.Coordinate.ToString(),
log);
}

break;

case MutableEnumTypeDefinition enumType:
foreach (var value in enumType.Values)
{
ValidateProvider(
schema,
value,
DirectiveLocation.EnumValue,
value.Coordinate.ToString(),
log);
}

break;
}
}

foreach (var directiveDefinition in schema.DirectiveDefinitions)
{
ValidateProvider(
schema,
directiveDefinition,
DirectiveLocation.DirectiveDefinition,
$"@{directiveDefinition.Name}",
log);

foreach (var argument in directiveDefinition.Arguments)
{
ValidateProvider(
schema,
argument,
DirectiveLocation.ArgumentDefinition,
$"@{directiveDefinition.Name}({argument.Name}:)",
log);
}
}
}

private static void ValidateProvider(
MutableSchemaDefinition schema,
IDirectivesProvider provider,
DirectiveLocation location,
string member,
ICompositionLog log)
{
foreach (var directive in provider.Directives)
{
if (!s_signatures.TryGetValue(directive.Name, out var signature))
{
continue;
}

if ((signature.Locations & location) == 0)
{
log.Write(
LogEntryBuilder.New()
.SetMessage(
FederationV1SchemaAnalyzer_InvalidDirectiveLocation,
schema.Name,
directive.Name,
member)
.SetCode(LogEntryCodes.FederationV1DirectiveNotSupported)
.SetSeverity(LogSeverity.Error)
.SetSchema(schema)
.SetTypeSystemMember(provider)
.Build());
}

var argumentsAreValid = signature.RequiredArgument is { } requiredArgument
? directive.Arguments.Count == 1
&& directive.Arguments.ContainsName(requiredArgument)
: directive.Arguments.Count == 0;

if (!argumentsAreValid)
{
log.Write(
LogEntryBuilder.New()
.SetMessage(
FederationV1SchemaAnalyzer_InvalidDirectiveArguments,
schema.Name,
directive.Name,
member,
signature.ExpectedArguments)
.SetCode(LogEntryCodes.FederationV1DirectiveNotSupported)
.SetSeverity(LogSeverity.Error)
.SetSchema(schema)
.SetTypeSystemMember(provider)
.Build());
}
}
}

private static DirectiveLocation GetTypeLocation(ITypeDefinition type)
=> type switch
{
IObjectTypeDefinition => DirectiveLocation.Object,
IInterfaceTypeDefinition => DirectiveLocation.Interface,
IUnionTypeDefinition => DirectiveLocation.Union,
IScalarTypeDefinition => DirectiveLocation.Scalar,
IEnumTypeDefinition => DirectiveLocation.Enum,
IInputObjectTypeDefinition => DirectiveLocation.InputObject,
_ => 0
};

private sealed record DirectiveSignature(
DirectiveLocation Locations,
string? RequiredArgument,
string ExpectedArguments);
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,13 @@
<data name="FederationSchemaAnalyzer_DirectiveNotSupported" xml:space="preserve">
<value>The @{0} directive is not supported.</value>
</data>
<data name="FederationSchemaTransformer_ParseFailed" xml:space="preserve">
<data name="FederationV1SchemaAnalyzer_InvalidDirectiveArguments" xml:space="preserve">
<value>Apollo Federation v1 source schema '{0}' applies '@{1}' to '{2}' with invalid arguments. Expected {3}.</value>
</data>
<data name="FederationV1SchemaAnalyzer_InvalidDirectiveLocation" xml:space="preserve">
<value>Apollo Federation v1 source schema '{0}' applies '@{1}' at unsupported location '{2}'.</value>
</data>
<data name="FederationSchemaTransformer_ParseFailed" xml:space="preserve">
<value>Failed to parse federation SDL: {0}</value>
</data>
</root>
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using HotChocolate.Types.Mutable;

namespace HotChocolate.Fusion.ApolloFederation;

/// <summary>
/// Removes an empty query root from a mutable source schema.
/// </summary>
internal static class RemoveEmptyQueryRoot
{
public static void Apply(MutableSchemaDefinition schema)
{
if (schema.QueryType is not { Fields.Count: 0 } emptyQueryRoot)
{
return;
}

schema.Types.Remove(emptyQueryRoot);
schema.QueryType = null;
}
}
Loading
Loading