diff --git a/src/Nitrolize.Tests/Integration/Schema/EntityA.cs b/src/Nitrolize.Tests/Integration/Schema/EntityA.cs new file mode 100644 index 0000000..46b568f --- /dev/null +++ b/src/Nitrolize.Tests/Integration/Schema/EntityA.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; + +namespace Nitrolize.Tests.Integration.Schema +{ + public class EntityA + { + public Guid Id { get; set; } + + public string Name { get; set; } + + public List Entities { get; set; } + } +} diff --git a/src/Nitrolize.Tests/Integration/Schema/EntityB.cs b/src/Nitrolize.Tests/Integration/Schema/EntityB.cs new file mode 100644 index 0000000..cfc33a6 --- /dev/null +++ b/src/Nitrolize.Tests/Integration/Schema/EntityB.cs @@ -0,0 +1,11 @@ +using System; + +namespace Nitrolize.Tests.Integration.Schema +{ + public class EntityB + { + public Guid Id { get; set; } + + public double Value { get; set; } + } +} diff --git a/src/Nitrolize.Tests/Integration/Schema/Mutation.cs b/src/Nitrolize.Tests/Integration/Schema/Mutation.cs new file mode 100644 index 0000000..c84482e --- /dev/null +++ b/src/Nitrolize.Tests/Integration/Schema/Mutation.cs @@ -0,0 +1,8 @@ +using Nitrolize.Types.Base; + +namespace Nitrolize.Tests.Integration.Schema +{ + public class Mutation : MutationBase + { + } +} diff --git a/src/Nitrolize.Tests/Integration/Schema/Query.cs b/src/Nitrolize.Tests/Integration/Schema/Query.cs new file mode 100644 index 0000000..0074a00 --- /dev/null +++ b/src/Nitrolize.Tests/Integration/Schema/Query.cs @@ -0,0 +1,14 @@ +using GraphQL.Types; +using Nitrolize.Extensions; + +namespace Nitrolize.Tests.Integration.Schema +{ + public class Query : ObjectGraphType + { + public Query() + { + this.Field("viewer", resolve: context => new Viewer()) + .RequiresAuthentication(false); + } + } +} diff --git a/src/Nitrolize.Tests/Integration/Schema/Schema.cs b/src/Nitrolize.Tests/Integration/Schema/Schema.cs new file mode 100644 index 0000000..77a6950 --- /dev/null +++ b/src/Nitrolize.Tests/Integration/Schema/Schema.cs @@ -0,0 +1,11 @@ +namespace Nitrolize.Tests.Integration.Schema +{ + public class Schema : GraphQL.Types.Schema + { + public Schema() + { + this.Query = new Query(); + this.Mutation = new Mutation(); + } + } +} diff --git a/src/Nitrolize.Tests/Integration/Schema/Viewer.cs b/src/Nitrolize.Tests/Integration/Schema/Viewer.cs new file mode 100644 index 0000000..eb2f92b --- /dev/null +++ b/src/Nitrolize.Tests/Integration/Schema/Viewer.cs @@ -0,0 +1,15 @@ +using Nitrolize.Identification; +using System; + +namespace Nitrolize.Tests.Integration.Schema +{ + public class Viewer + { + public string Id { get; set; } + + public Viewer() + { + this.Id = GlobalId.ToGlobalId("Viewer", Guid.NewGuid().ToString()); + } + } +} diff --git a/src/Nitrolize.Tests/Integration/Schema/ViewerType.cs b/src/Nitrolize.Tests/Integration/Schema/ViewerType.cs new file mode 100644 index 0000000..d95ec28 --- /dev/null +++ b/src/Nitrolize.Tests/Integration/Schema/ViewerType.cs @@ -0,0 +1,56 @@ +using Nitrolize.Convenience.Attributes; +using Nitrolize.Convenience.Delegates; +using Nitrolize.Models; +using Nitrolize.Types.Base; +using System; +using System.Collections.Generic; + +namespace Nitrolize.Tests.Integration.Schema +{ + public class ViewerType : ViewerTypeBase + { + [Field(IsAuthenticationRequired = false)] + public Field EntityA => (context, id) => + { + var entity = new EntityA + { + Id = new Guid(), + Name = "The Entity A", + Entities = new List() + { + new EntityB { Id = new Guid(), Value = 1.1 }, + new EntityB { Id = new Guid(), Value = 2.2 } + } + }; + return entity; + }; + + [List(IsAuthenticationRequired = false)] + public ListField EntityList => (context) => + { + var list = new List() + { + new EntityA { Id = new Guid(), Name = "No1" }, + new EntityA { Id = new Guid(), Name = "No2" } + }; + + return list; + }; + + [Connection(IsAuthenticationRequired = false)] + public ConnectionField EntityConnection => (context, parameters) => + { + var list = new List() + { + new EntityA { Id = new Guid(), Name = "No1" }, + new EntityA { Id = new Guid(), Name = "No2" } + }; + var connection = new Connection(list); + + connection.PageInfo.HasPreviousPage = false; + connection.PageInfo.HasNextPage = true; + + return connection; + }; + } +} diff --git a/src/Nitrolize.Tests/Integration/ViewerTypeSpecification.cs b/src/Nitrolize.Tests/Integration/ViewerTypeSpecification.cs new file mode 100644 index 0000000..d0ff31a --- /dev/null +++ b/src/Nitrolize.Tests/Integration/ViewerTypeSpecification.cs @@ -0,0 +1,127 @@ +using FluentAssertions; +using GraphQL; +using GraphQL.Http; +using GraphQL.Instrumentation; +using GraphQL.Validation.Complexity; +using Machine.Fakes; +using Machine.Specifications; +using Nitrolize.Types.Base; +using TestSchema = Nitrolize.Tests.Integration.Schema; + +namespace Nitrolize.Tests.Schema +{ + [Subject(typeof(ViewerTypeBase))] + public class ViewerTypeSpecification : WithSubject + { + protected static IDocumentExecuter DocumentExecuter = new DocumentExecuter(); + protected static IDocumentWriter DocumentWriter = new DocumentWriter(true); + protected static dynamic Result; + + protected static object Execute(string query) + { + var result = DocumentExecuter.ExecuteAsync(_ => + { + _.Schema = new TestSchema.Schema(); + _.Query = query; + _.OperationName = null; + _.Inputs = new Inputs(); + + _.ComplexityConfiguration = new ComplexityConfiguration { MaxDepth = 15 }; + _.FieldMiddleware.Use(); + _.UserContext = null; + _.ValidationRules = null; + }).Await().AsTask.Result; + + return result.Data; + } + } + + public class When_querying_a_field : ViewerTypeSpecification + { + protected static string Query = @" + { + viewer { + entityA(id: ""VXNlciNmOTM2OGNlNC0wNjhkLTQxN2ItYmZiZi0wMDdkMzEyYTA4ZmM="") { + id + name + } + } + }"; + + Because of = () => Result = Execute(Query); + + It should_return_a_property = () => { + var name = (string)Result["viewer"]["entityA"]["name"]; + name.Should().Be("The Entity A"); + }; + } + + public class When_querying_a_list : ViewerTypeSpecification + { + protected static string Query = @" + { + viewer { + entityList { + id + name + } + } + } + "; + + Because of = () => Result = Execute(Query); + + It should_return_items = () => ((object)Result["viewer"]["entityList"]).Should().NotBeNull(); + + It should_return_first_item_name = () => ((object)Result["viewer"]["entityList"][0]["name"]).Should().Be("No1"); + + It should_return_second_item_name = () => ((object)Result["viewer"]["entityList"][1]["name"]).Should().Be("No2"); + } + + public class When_querying_a_connection : ViewerTypeSpecification + { + protected static string Query = @" + { + viewer { + entityConnection(first: 100) { + edges { + cursor + node { + id + name + } + } + pageInfo { + startCursor + endCursor + hasPreviousPage + hasNextPage + } + } + } + } + "; + + Because of = () => Result = Execute(Query); + + It should_return_page_info = () => ((object)Result["viewer"]["entityConnection"]["pageInfo"]).Should().NotBeNull(); + + It should_return_page_info_start_cursor = () => ((object)Result["viewer"]["entityConnection"]["pageInfo"]["startCursor"]).Should().NotBeNull(); + + It should_return_page_info_end_cursor = () => ((object)Result["viewer"]["entityConnection"]["pageInfo"]["endCursor"]).Should().NotBeNull(); + + It should_return_page_info_hasPreviousPage = () => ((object)Result["viewer"]["entityConnection"]["pageInfo"]["hasPreviousPage"]).Should().Be(false); + + It should_return_page_info_hasNextPage = () => ((object)Result["viewer"]["entityConnection"]["pageInfo"]["hasNextPage"]).Should().Be(true); + + It should_return_edges = () => ((object)Result["viewer"]["entityConnection"]["edges"]).Should().NotBeNull(); + + It should_return_first_edge_cursor = () => ((object)Result["viewer"]["entityConnection"]["edges"][0]["cursor"]).Should().NotBeNull(); + + It should_return_first_edge_node_name = () => ((object)Result["viewer"]["entityConnection"]["edges"][0]["node"]["name"]).Should().Be("No1"); + + It should_return_second_edge_cursor = () => ((object)Result["viewer"]["entityConnection"]["edges"][1]["cursor"]).Should().NotBeNull(); + + It should_return_second_edge_node_name = () => ((object)Result["viewer"]["entityConnection"]["edges"][1]["node"]["name"]).Should().Be("No2"); + } +} diff --git a/src/Nitrolize.Tests/TypeExtensionsSpecification.cs b/src/Nitrolize.Tests/TypeExtensionsSpecification.cs index 82bf134..7537625 100644 --- a/src/Nitrolize.Tests/TypeExtensionsSpecification.cs +++ b/src/Nitrolize.Tests/TypeExtensionsSpecification.cs @@ -68,7 +68,7 @@ public class ExampleClass It should_have_set_a_correct_name = () => Result.Name.Should().Be("GenericClassExampleClass"); - It should_derive_from_original_type = () => Result.IsSubclassOf(typeof(GenericClass)).Should().BeTrue(); + It should_derive_from_original_type = () => Result.GetTypeInfo().IsSubclassOf(typeof(GenericClass)).Should().BeTrue(); } public abstract class GettingIdSpecification : TypeExtensionsSpecification @@ -178,9 +178,9 @@ public class InputModel : Model It should_convert_ids_of_list_items = () => Result.GetProperty("Items").PropertyType.GetGenericArguments()[0].GetProperty("Id").PropertyType.Name.Should().Be("String"); - It should_have_created_class_attribute = () => Result.GetCustomAttributes().Should().NotBeEmpty(); + It should_have_created_class_attribute = () => Result.GetTypeInfo().GetCustomAttributes().Should().NotBeEmpty(); - It should_have_stored_original_id_type_in_class_attribute = () => Result.GetCustomAttribute().TypeName.Should().Contain("Guid"); + It should_have_stored_original_id_type_in_class_attribute = () => Result.GetTypeInfo().GetCustomAttribute().TypeName.Should().Contain("Guid"); It should_have_omitted_id_property = () => ResultWithoutId.GetProperty("Id").Should().BeNull(); } diff --git a/src/Nitrolize.Tests/project.json b/src/Nitrolize.Tests/project.json index 0da90dc..50623aa 100644 --- a/src/Nitrolize.Tests/project.json +++ b/src/Nitrolize.Tests/project.json @@ -6,13 +6,35 @@ "Machine.Fakes.Moq": "2.8.0", "Machine.Specifications": "0.11.0", "NETStandard.Library": "1.6.1", - "Nitrolize": "0.1.2" + "Nitrolize": "0.2.0-*", + "System.Reflection": "4.3.0" }, "frameworks": { "net461": { "imports": "dnxcore50" + }, + "netcoreapp1.0": { + "imports": [ + "dotnet5.6", + "portable-net45+win8" + ], + "dependencies": { + "Microsoft.Extensions.DependencyModel": "1.1.1", + "System.ComponentModel.TypeConverter": "4.3.0", + "System.Reflection.Emit": "4.3.0" + }, + "buildOptions": { + "define": [ + "NETCOREAPP1_0" + ] + } } }, + + "runtimes": { + "win10-x64": {} + }, + "configurations": { "NuGet": {} } diff --git a/src/Nitrolize/Convenience/Attributes/ListAttribute.cs b/src/Nitrolize/Convenience/Attributes/ListAttribute.cs new file mode 100644 index 0000000..33e6df7 --- /dev/null +++ b/src/Nitrolize/Convenience/Attributes/ListAttribute.cs @@ -0,0 +1,15 @@ +using System; + +namespace Nitrolize.Convenience.Attributes +{ + /// + /// Speficies that the following property is a relay compatible connection. + /// + [AttributeUsage(AttributeTargets.Property)] + public class ListAttribute : AuthenticationRequiredAttributeBase + { + public ListAttribute() + { + } + } +} diff --git a/src/Nitrolize/Convenience/Delegates/ListField.cs b/src/Nitrolize/Convenience/Delegates/ListField.cs new file mode 100644 index 0000000..e510d5e --- /dev/null +++ b/src/Nitrolize/Convenience/Delegates/ListField.cs @@ -0,0 +1,11 @@ +using GraphQL.Types; + +namespace Nitrolize.Convenience.Delegates +{ + /// + /// Delegate for list fields. + /// + /// The type of the entity. + /// The ResolveFieldContext. + public delegate object ListField(ResolveFieldContext context); +} diff --git a/src/Nitrolize/Extensions/ObjectExtensions.cs b/src/Nitrolize/Extensions/ObjectExtensions.cs index d19ce92..304e846 100644 --- a/src/Nitrolize/Extensions/ObjectExtensions.cs +++ b/src/Nitrolize/Extensions/ObjectExtensions.cs @@ -200,7 +200,7 @@ private static T CloneProperties(object subject, T clone, bool omitIdProperty return clone; } - public static IEnumerable GetPropertiesWithAttribute(this Object subject) where TAttribute : Attribute + public static IEnumerable GetPropertiesWithAttribute(this object subject) where TAttribute : Attribute { return subject.GetType().GetProperties().Where(p => p.GetCustomAttributes(typeof(TAttribute), false).Any()); } diff --git a/src/Nitrolize/Extensions/TypeExtensions.cs b/src/Nitrolize/Extensions/TypeExtensions.cs index bf11e97..f3c5b26 100644 --- a/src/Nitrolize/Extensions/TypeExtensions.cs +++ b/src/Nitrolize/Extensions/TypeExtensions.cs @@ -376,7 +376,6 @@ public static Type MapToGraphType(this Type type) // handle object types return typeof(ObjectType<>).MakeGenericType(type); - // throw new NotImplementedException($"There is no graph type mapping implemented for {type.Name}."); } public static bool IsSimpleType(this Type type) @@ -418,26 +417,6 @@ public static bool IsGenericType(this Type type) return type.IsGenericType; #endif } - -//#if NETCOREAPP1_0 -// public static MethodInfo[] GetMethods(this Type type) -// { -// return type.GetTypeInfo().GetMethods(); -// } - -// public static MethodInfo GetMethod(this Type type, string name) -// { -// return type.GetTypeInfo().GetMethod(name); -// } - -// public static Type GetInterface(this Type type, string name) -// { -// return type.GetTypeInfo().GetInterface(name); -// } - -//#endif - - #endregion } } diff --git a/src/Nitrolize/Identification/GlobalId.cs b/src/Nitrolize/Identification/GlobalId.cs index aaf2aff..6581625 100644 --- a/src/Nitrolize/Identification/GlobalId.cs +++ b/src/Nitrolize/Identification/GlobalId.cs @@ -1,5 +1,4 @@ -using Nitrolize.Extensions; -using System; +using System; using System.Linq; using System.Reflection; diff --git a/src/Nitrolize/Types/Base/ViewerTypeBase.cs b/src/Nitrolize/Types/Base/ViewerTypeBase.cs index 667cee8..a8feab9 100644 --- a/src/Nitrolize/Types/Base/ViewerTypeBase.cs +++ b/src/Nitrolize/Types/Base/ViewerTypeBase.cs @@ -19,6 +19,7 @@ public abstract class ViewerTypeBase : NodeType protected ViewerTypeBase() { this.FindAndConvertPropertiesToFields(); + this.FindAndConvertPropertiesToLists(); this.FindAndConvertPropertiesToConnections(); } @@ -79,14 +80,14 @@ private void ConvertPropertyToGetByIdField(PropertyInfo field) Func, object> resolve = (context) => { // get the connection delegate - var method = ((Delegate)(field.GetValue(this))).GetMethodInfo(); + var method = ((Delegate)(field.GetValue(this))); // convert the global id to local id var globalId = context.GetArgument(entityType.GetIdPropertyName().ToFirstLower()); var id = GlobalId.ToLocalId(idType, globalId); // invoke field with "context" and "id" - return method.Invoke(this, new object[] { context, id }); + return method.DynamicInvoke(new object[] { context, id }); }; // handle authentication and authorization @@ -98,6 +99,33 @@ private void ConvertPropertyToGetByIdField(PropertyInfo field) graphQLField.RequiresAuthentication(isAuthenticationRequired); } + private void FindAndConvertPropertiesToLists() + { + // find all list properties + var lists = this.GetPropertiesWithAttribute(); + foreach (var list in lists) + { + // construct ListGraphType + var entityType = list.PropertyType.GetGenericArguments()[0].MapToGraphType(); + var listGraphType = typeof(ListGraphType<>).MakeGenericType(entityType); + + // construct resolving method + Func, object> resolve = (context) => + { + var method = ((Delegate)(list.GetValue(this))); + return method.DynamicInvoke(new object[] { context }); + }; + + // handle authentication and authorization + var isAuthenticationRequired = list.GetAttribute().IsAuthenticationRequired; + var requiredRoles = list.GetRequiredRoles(); + + var graphQLField = this.Field(listGraphType, list.Name.ToFirstLower(), null, new QueryArguments(), resolve); + graphQLField.RequiresRoles(requiredRoles); + graphQLField.RequiresAuthentication(isAuthenticationRequired); + } + } + private void FindAndConvertPropertiesToConnections() { // find all connection properties @@ -113,7 +141,7 @@ private void FindAndConvertPropertiesToConnections() Func, object> resolve = (context) => { // get the connection delegate - var method = ((Delegate)(connection.GetValue(this))).GetMethodInfo(); + var method = ((Delegate)(connection.GetValue(this))); // get order by infos var orderByValue = context.GetArgument("orderBy"); @@ -144,7 +172,7 @@ private void FindAndConvertPropertiesToConnections() }); // invoke connection with "context" and "parameters" - return method.Invoke(this, new object[] { context, parameters }); + return method.DynamicInvoke(new object[] { context, parameters }); }; // handle authentication and authorization diff --git a/src/Nitrolize/project.json b/src/Nitrolize/project.json index 0bf25b2..c7577da 100644 --- a/src/Nitrolize/project.json +++ b/src/Nitrolize/project.json @@ -1,11 +1,11 @@ { "name": "Nitrolize", - "version": "0.1.3-*", + "version": "0.2.0-*", "title": "Nitrolize", "authors": [ "progresso group" ], "packOptions": { "owners": [ "progresso group" ], - "releaseNotes": "Added support for .NETCoreApp", + "releaseNotes": "Added convenience support for List Graph Type", "projectUrl": "https://github.com/progresso-group/Nitrolize", "iconUrl": "https://raw.githubusercontent.com/progresso-group/Nitrolize/master/static/nitrolize-nuget-logo.png", "licenseUrl": "https://www.gnu.org/licenses/agpl-3.0.de.html",