Skip to content

Commit

Permalink
Enhancement: Easy way to create validation rules with a custom FieldT…
Browse files Browse the repository at this point in the history
…ype MetaDataAttribute (#209)

* Revert Make GraphTypeInfo.TypeParameter lazy (#8)

* Revert Make GraphTypeInfo.TypeParameter lazy

* Version bumps

* Fix version bump for GraphQL

* "Unexpected type: " error fix

hacky fix for "Unexpected type: " error when return type of resolver method is INode

* Restore 'GraphTypeInfo'

* Added unique version suffix 

we needed a way to have unique name for out own nuget feed

* commetns resolved

* removed version suffix

* set a proper csproj file version

* added support to add a FieldType metadata information

* fixed csproj version

* proper csproj version

* updated logic to obtain metadata

* Added proper release version

* fixed typo in test method

Co-authored-by: K-Pavlov <[email protected]>
Co-authored-by: Rob Wijkstra <[email protected]>
Co-authored-by: Rob Wijkstra <[email protected]>
  • Loading branch information
4 people authored Oct 28, 2020
1 parent 76d9b0c commit 1238a01
Show file tree
Hide file tree
Showing 5 changed files with 194 additions and 3 deletions.
12 changes: 12 additions & 0 deletions src/GraphQL.Conventions/Adapters/GraphTypeAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ private FieldType DeriveField(GraphFieldInfo fieldInfo)
Name = fieldInfo.Name,
Description = fieldInfo.Description,
DeprecationReason = fieldInfo.DeprecationReason,
Metadata = DeriveFieldTypeMetaData(fieldInfo.AttributeProvider.GetCustomAttributes(false)),
DefaultValue = fieldInfo.DefaultValue,
Type = GetType(fieldInfo.Type),
Arguments = new QueryArguments(fieldInfo.Arguments.Where(arg => !arg.IsInjected).Select(DeriveArgument)),
Expand All @@ -103,12 +104,23 @@ private FieldType DeriveField(GraphFieldInfo fieldInfo)
Description = fieldInfo.Description,
DeprecationReason = fieldInfo.DeprecationReason,
DefaultValue = fieldInfo.DefaultValue,
Metadata = DeriveFieldTypeMetaData(fieldInfo.AttributeProvider.GetCustomAttributes(false)),
Type = GetType(fieldInfo.Type),
Arguments = new QueryArguments(fieldInfo.Arguments.Where(arg => !arg.IsInjected).Select(DeriveArgument)),
Resolver = FieldResolverFactory(fieldInfo),
};
}

private IDictionary<string, object> DeriveFieldTypeMetaData(object[] attributes)
{
if (attributes == null)
return default;

return attributes
.OfType<FieldTypeMetaDataAttribute>()
.ToDictionary(x => x.Key(), x => x.Value());
}

private QueryArgument DeriveArgument(GraphArgumentInfo argumentInfo)
{
return new QueryArgument(GetType(argumentInfo.Type))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System;

namespace GraphQL.Conventions
{
[AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = true)]
public abstract class FieldTypeMetaDataAttribute : GraphQLAttribute
{
public abstract string Key();

public abstract object Value();
}
}
4 changes: 2 additions & 2 deletions src/GraphQL.Conventions/CommonAssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
[assembly: AssemblyCopyright("Copyright 2016-2019 Tommy Lillehagen. All rights reserved.")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("2.5.4")]
[assembly: AssemblyInformationalVersion("2.5.4")]
[assembly: AssemblyFileVersion("2.5.5")]
[assembly: AssemblyInformationalVersion("2.5.5")]
[assembly: CLSCompliant(false)]

[assembly: InternalsVisibleTo("Tests")]
2 changes: 1 addition & 1 deletion src/GraphQL.Conventions/GraphQL.Conventions.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<Description>GraphQL Conventions for .NET</Description>
<VersionPrefix>2.5.4</VersionPrefix>
<VersionPrefix>2.5.5</VersionPrefix>
<Authors>Tommy Lillehagen</Authors>
<TargetFrameworks>net45;netstandard1.6.1</TargetFrameworks>
<DepsFileGenerationMode>old</DepsFileGenerationMode>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
using GraphQL.Conventions.Execution;
using GraphQL.Conventions.Tests.Templates;
using GraphQL.Conventions.Tests.Templates.Extensions;
using GraphQL.Language.AST;
using GraphQL.Validation;
using System;
using System.Linq;
using System.Threading.Tasks;

namespace GraphQL.Conventions.Tests.Attributes.MetaData
{
public class FieldTypeMetaDataAttributeTests : TestBase
{
[Test]
public void TopLevelQuery_Should_Have_Correct_CustomAttributeValue()
{
var type = TypeInfo<CustomAttribute_Query>();
var field = type.ShouldHaveFieldWithName("node");
var customAttributes = field.AttributeProvider.GetCustomAttributes(false);
var result = customAttributes.FirstOrDefault(x => x.GetType() == typeof(TestCustomAttribute)) as TestCustomAttribute;
Assert.IsTrue(result != null);
Assert.IsTrue(result.Permission == nameof(SomeTopLevelValidation));
}

[Test]
public void ClassMethod_Should_Have_Correct_CustomAttributeValue()
{
var type = TypeInfo<TestType>();
var field = type.ShouldHaveFieldWithName("testMethod");
var customAttributes = field.AttributeProvider.GetCustomAttributes(false);
var result = customAttributes.FirstOrDefault(x => x.GetType() == typeof(TestCustomAttribute)) as TestCustomAttribute;
Assert.IsTrue(result != null);
Assert.IsTrue(result.Permission == nameof(SomeMethodValidation));
}

[Test]
public void ClassMethod_Without_CustomAttribute_ShouldBeNull()
{
var type = TypeInfo<TestType>();
var field = type.ShouldHaveFieldWithName("noAttribute");
var customAttributes = field.AttributeProvider.GetCustomAttributes(false);
var result = customAttributes.FirstOrDefault(x => x.GetType() == typeof(TestCustomAttribute)) as TestCustomAttribute;
Assert.IsTrue(result == null);
}

[Test]
public async Task When_UserIsMissingAll_ValidationFieldTypeMetaData_ErrorsAreReturned()
{
var result = await Resolve_Query();
result.ShouldHaveErrors(2);
var expectedMessages = new[]
{
$"Required validation '{nameof(SomeTopLevelValidation)}' is not present. Query will not be executed.",
$"Required validation '{nameof(SomeMethodValidation)}' is not present. Query will not be executed."
};

var messages = result.Errors.Select(e => e.Message);
Assert.IsTrue(messages.All(x => expectedMessages.Contains(x)));
}

[Test]
public async Task When_UserHas_Requested_ValidationFieldTypeMetaData_ThereAreNoErrors()
{
var result = await Resolve_Query(selectedFields: "noAttribute name", accessPermissions: nameof(SomeTopLevelValidation));
result.ShouldHaveNoErrors();
}

[Test]
public async Task When_UserHasAll_Requested_ValidationFieldTypeMetaData_ThereAreNoErrors()
{
var result = await Resolve_Query(accessPermissions: new[] { nameof(SomeTopLevelValidation), nameof(SomeMethodValidation) });
result.ShouldHaveNoErrors();
}

private async Task<ExecutionResult> Resolve_Query(string selectedFields = "testMethod noAttribute name", params string[] accessPermissions)
{
var engine = GraphQLEngine
.New<CustomAttribute_Query>();

var user = new TestUserContext(accessPermissions);

var result = await engine
.NewExecutor()
.WithQueryString("query { node { " + selectedFields + " } }")
.WithUserContext(user)
.WithValidationRules(new[] { new TestValidation() })
.Execute();

return result;
}
}

public class TestUserContext : IUserContext
{
public TestUserContext(params string[] accessPermissions)
{
AccessPermissions = accessPermissions;
}

public string[] AccessPermissions { get; }
}

public class CustomAttribute_Query
{
[TestCustom(nameof(SomeTopLevelValidation))]
public TestType Node() => new TestType();
}

public class TestType
{
[TestCustom(nameof(SomeMethodValidation))]
public string TestMethod() => "TestMethod";

public string NoAttribute() => "NoAttribute";

public string Name { get; set; } = "TestType";
}

public class SomeTopLevelValidation { }

public class SomeMethodValidation { }

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class TestCustomAttribute : FieldTypeMetaDataAttribute
{
public TestCustomAttribute(string permission)
{
Permission = permission;
}

public string Permission { get; }

public override string Key() => nameof(TestCustomAttribute);

public override object Value() => Permission;
}

public class TestValidation : IValidationRule
{
public INodeVisitor Validate(ValidationContext context)
{
var userContext = context.UserContext as UserContextWrapper;
var user = userContext.UserContext as TestUserContext;

return new EnterLeaveListener(_ =>
{
_.Match<Field>(node =>
{
var fieldDef = context.TypeInfo.GetFieldDef();
if (fieldDef == null) return;
if (fieldDef.Metadata != null && fieldDef.HasMetadata(nameof(TestCustomAttribute)))
{
var permissionMetaData = fieldDef.Metadata.First(x => x.Key == nameof(TestCustomAttribute));
var requiredValidation = permissionMetaData.Value as string;

if(!user.AccessPermissions.Any(p => p == requiredValidation))
context.ReportError(new ValidationError( /* When reporting such errors no data would be returned use with cautious */
context.OriginalQuery,
"Authorization",
$"Required validation '{requiredValidation}' is not present. Query will not be executed.",
node));
}
});
});
}
}
}

0 comments on commit 1238a01

Please sign in to comment.