diff --git a/src/OrchardCore.Modules/OrchardCore.Apis.GraphQL/GraphQLMiddleware.cs b/src/OrchardCore.Modules/OrchardCore.Apis.GraphQL/GraphQLMiddleware.cs index 44281d94b6d..c988122c5fe 100644 --- a/src/OrchardCore.Modules/OrchardCore.Apis.GraphQL/GraphQLMiddleware.cs +++ b/src/OrchardCore.Modules/OrchardCore.Apis.GraphQL/GraphQLMiddleware.cs @@ -4,7 +4,6 @@ using System.Net; using System.Net.Mime; using System.Text; -using System.Text.Json; using System.Threading.Tasks; using GraphQL; using GraphQL.Execution; @@ -28,6 +27,7 @@ namespace OrchardCore.Apis.GraphQL public class GraphQLMiddleware : IMiddleware { private readonly GraphQLSettings _settings; + private readonly IGraphQLTextSerializer _graphQLTextSerializer; private readonly IGraphQLSerializer _serializer; private readonly IDocumentExecuter _executer; internal static readonly Encoding _utf8Encoding = new UTF8Encoding(false); @@ -37,11 +37,13 @@ public class GraphQLMiddleware : IMiddleware public GraphQLMiddleware( IOptions settingsOption, IDocumentExecuter executer, - IGraphQLSerializer serializer) + IGraphQLSerializer serializer, + IGraphQLTextSerializer graphQLTextSerializer) { _settings = settingsOption.Value; _executer = executer; _serializer = serializer; + _graphQLTextSerializer = graphQLTextSerializer; } public async Task InvokeAsync(HttpContext context, RequestDelegate next) { @@ -89,11 +91,9 @@ private async Task ExecuteAsync(HttpContext context) if (mediaType.IsSubsetOf(_jsonMediaType) || mediaType.IsSubsetOf(_graphQlMediaType)) { - + using var sr = new StreamReader(context.Request.Body); if (mediaType.IsSubsetOf(_graphQlMediaType)) { - using var sr = new StreamReader(context.Request.Body); - request = new GraphQLNamedQueryRequest { Query = await sr.ReadToEndAsync() @@ -101,7 +101,7 @@ private async Task ExecuteAsync(HttpContext context) } else { - request = await JsonSerializer.DeserializeAsync(context.Request.Body, JOptions.CamelCase); + request = _graphQLTextSerializer.Deserialize(await sr.ReadToEndAsync()); } } else @@ -171,7 +171,7 @@ private async Task ExecuteAsync(HttpContext context) await _serializer.WriteAsync(context.Response.Body, result); } - private static GraphQLNamedQueryRequest CreateRequestFromQueryString(HttpContext context, bool validateQueryKey = false) + private GraphQLNamedQueryRequest CreateRequestFromQueryString(HttpContext context, bool validateQueryKey = false) { if (!context.Request.Query.ContainsKey("query")) { @@ -190,7 +190,7 @@ private static GraphQLNamedQueryRequest CreateRequestFromQueryString(HttpContext if (context.Request.Query.ContainsKey("variables")) { - request.Variables = JsonSerializer.Deserialize(context.Request.Query["variables"], JOptions.CamelCase); + request.Variables = _graphQLTextSerializer.Deserialize(context.Request.Query["variables"]); } if (context.Request.Query.ContainsKey("operationName")) diff --git a/src/OrchardCore.Modules/OrchardCore.Apis.GraphQL/Json/GraphQLNamedQueryRequestJsonConverter.cs b/src/OrchardCore.Modules/OrchardCore.Apis.GraphQL/Json/GraphQLNamedQueryRequestJsonConverter.cs new file mode 100644 index 00000000000..3db29857c7d --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Apis.GraphQL/Json/GraphQLNamedQueryRequestJsonConverter.cs @@ -0,0 +1,121 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; +using GraphQL; + +namespace OrchardCore.Apis.GraphQL.Json; + +public class GraphQLNamedQueryRequestJsonConverter : JsonConverter +{ + public static readonly GraphQLNamedQueryRequestJsonConverter Instance = new(); + + /// + /// Name for the operation name parameter. + /// See https://github.com/graphql/graphql-over-http/blob/master/spec/GraphQLOverHTTP.md#request-parameters + /// + private const string _operationNameKey = "operationName"; + + /// + /// Name for the query parameter. + /// See https://github.com/graphql/graphql-over-http/blob/master/spec/GraphQLOverHTTP.md#request-parameters + /// + private const string _queryKey = "query"; + + /// + /// Name for the variables parameter. + /// See https://github.com/graphql/graphql-over-http/blob/master/spec/GraphQLOverHTTP.md#request-parameters + /// + private const string _variablesKey = "variables"; + + /// + /// Name for the extensions parameter. + /// See https://github.com/graphql/graphql-over-http/blob/master/spec/GraphQLOverHTTP.md#request-parameters + /// + private const string _extensionsKey = "extensions"; + + private const string _namedQueryKey = "namedQuery"; + + + public override void Write(Utf8JsonWriter writer, GraphQLNamedQueryRequest value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + if (value.Query != null) + { + writer.WritePropertyName(_queryKey); + writer.WriteStringValue(value.Query); + } + if (value.OperationName != null) + { + writer.WritePropertyName(_operationNameKey); + writer.WriteStringValue(value.OperationName); + } + if (value.Variables != null) + { + writer.WritePropertyName(_variablesKey); + JsonSerializer.Serialize(writer, value.Variables, options); + } + if (value.Extensions != null) + { + writer.WritePropertyName(_extensionsKey); + JsonSerializer.Serialize(writer, value.Extensions, options); + } + if (value.NamedQuery != null) + { + writer.WritePropertyName(_namedQueryKey); + JsonSerializer.Serialize(writer, value.NamedQuery, options); + } + writer.WriteEndObject(); + } + + public override GraphQLNamedQueryRequest Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.StartObject) + throw new JsonException(); + + var request = new GraphQLNamedQueryRequest(); + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + return request; + } + + if (reader.TokenType != JsonTokenType.PropertyName) + { + throw new JsonException(); + } + + string key = reader.GetString()!; + + //unexpected end of data + if (!reader.Read()) + throw new JsonException(); + + switch (key) + { + case _queryKey: + request.Query = reader.GetString()!; + break; + case _operationNameKey: + request.OperationName = reader.GetString()!; + break; + case _namedQueryKey: + request.NamedQuery = reader.GetString(); + break; + case _variablesKey: + request.Variables = JsonSerializer.Deserialize(ref reader, options); + break; + case _extensionsKey: + request.Extensions = JsonSerializer.Deserialize(ref reader, options); + break; + default: + reader.Skip(); + break; + } + } + + //unexpected end of data + throw new JsonException(); + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Apis.GraphQL/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Apis.GraphQL/Startup.cs index 8bdf71dda32..fb97d811d05 100644 --- a/src/OrchardCore.Modules/OrchardCore.Apis.GraphQL/Startup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Apis.GraphQL/Startup.cs @@ -9,9 +9,13 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; +using OrchardCore.Apis.GraphQL.Json; using OrchardCore.Apis.GraphQL.Services; using OrchardCore.Apis.GraphQL.ValidationRules; using OrchardCore.Environment.Shell.Configuration; +using OrchardCore.Extensions; +using OrchardCore.Json; +using OrchardCore.Json.Extensions; using OrchardCore.Modules; using OrchardCore.Navigation; using OrchardCore.Security.Permissions; @@ -46,7 +50,19 @@ public override void ConfigureServices(IServiceCollection services) services.AddScoped(); services.AddTransient(); services.AddSingleton(); - services.AddGraphQL(builder => builder.AddSystemTextJson()); + services.Configure(options => + { + + }); + + services.AddGraphQL(builder => builder.AddSystemTextJson((options, sp) => + { + // Common types of converters are already configured in the assembly "GraphQL.SystemTextJson". + options.Converters.Add(GraphQLNamedQueryRequestJsonConverter.Instance); + + var contentSerializerJsonOptions = sp.GetRequiredService>().Value; + options.Merge(contentSerializerJsonOptions.SerializerOptions); + })); services.AddOptions().Configure((c, configuration) => { diff --git a/src/OrchardCore.Modules/OrchardCore.ContentFields/GraphQL/Startup.cs b/src/OrchardCore.Modules/OrchardCore.ContentFields/GraphQL/Startup.cs index 7baaf362ede..6eff7d9ab16 100644 --- a/src/OrchardCore.Modules/OrchardCore.ContentFields/GraphQL/Startup.cs +++ b/src/OrchardCore.Modules/OrchardCore.ContentFields/GraphQL/Startup.cs @@ -19,6 +19,7 @@ public override void ConfigureServices(IServiceCollection services) services.AddObjectGraphType(); services.AddObjectGraphType(); services.AddObjectGraphType(); + services.AddObjectGraphType(); } } } diff --git a/src/OrchardCore.Modules/OrchardCore.ContentFields/GraphQL/Types/UserPickerFieldQueryObjectType.cs b/src/OrchardCore.Modules/OrchardCore.ContentFields/GraphQL/Types/UserPickerFieldQueryObjectType.cs new file mode 100644 index 00000000000..07f253d4a85 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.ContentFields/GraphQL/Types/UserPickerFieldQueryObjectType.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; +using System.Linq; +using GraphQL; +using GraphQL.DataLoader; +using GraphQL.Types; +using Microsoft.Extensions.DependencyInjection; +using OrchardCore.Apis.GraphQL; +using OrchardCore.ContentFields.Fields; +using OrchardCore.ContentManagement; +using OrchardCore.Users.GraphQL; +using OrchardCore.Users.Indexes; +using OrchardCore.Users.Models; +using YesSql; +using YesSql.Services; + +namespace OrchardCore.ContentFields.GraphQL +{ + public class UserPickerFieldQueryObjectType : ObjectGraphType + { + public UserPickerFieldQueryObjectType(UserType userType) + { + Name = nameof(UserPickerField); + + Field, IEnumerable>("userIds") + .Description("user ids") + .PagingArguments() + .Resolve(x => + { + return x.Page(x.Source.UserIds); + }); + + Field, IEnumerable>("users") + .Type(new ListGraphType(userType)) + .Description("the user items") + .PagingArguments() + .ResolveAsync(x => + { + var userLoader = GetOrAddUserProfileByIdDataLoader(x); + return userLoader.LoadAsync(x.Page(x.Source.UserIds)).Then(itemResultSet => + { + return itemResultSet.SelectMany(x => x); + }); + }); + Field("user") + .Type(userType) + .Description("the first user") + .ResolveAsync(x => + { + var userLoader = GetOrAddUserProfileByIdDataLoader(x); + return userLoader.LoadAsync(x.Source.UserIds.FirstOrDefault()).Then(itemResultSet => + { + return itemResultSet.FirstOrDefault(); + }); + }); + } + + public static IDataLoader> GetOrAddUserProfileByIdDataLoader(IResolveFieldContext context) + { + IDataLoaderContextAccessor requiredService = context.RequestServices.GetRequiredService(); + var session = context.RequestServices.GetService(); + return requiredService.Context.GetOrAddCollectionBatchLoader("GetOrAddUserByIds", async (IEnumerable userIds) => + { + if (userIds == null || !userIds.Any()) + { + return null; + } + var users = await session.Query(y => y.UserId.IsIn(userIds)).ListAsync(); + + return users.ToLookup((User k) => k.UserId, (User user) => user); + }); + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.ContentFields/OrchardCore.ContentFields.csproj b/src/OrchardCore.Modules/OrchardCore.ContentFields/OrchardCore.ContentFields.csproj index d50cedac85f..1e75bfb884f 100644 --- a/src/OrchardCore.Modules/OrchardCore.ContentFields/OrchardCore.ContentFields.csproj +++ b/src/OrchardCore.Modules/OrchardCore.ContentFields/OrchardCore.ContentFields.csproj @@ -29,6 +29,7 @@ + diff --git a/src/OrchardCore.Modules/OrchardCore.Users/GraphQL/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Users/GraphQL/Startup.cs index 4dba4a80de3..85daf5d706f 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/GraphQL/Startup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/GraphQL/Startup.cs @@ -10,6 +10,6 @@ public class Startup : StartupBase public override void ConfigureServices(IServiceCollection services) { services.AddSingleton(); - services.AddTransient(); + services.AddScoped(); } } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/GraphQL/UserType.cs b/src/OrchardCore.Modules/OrchardCore.Users/GraphQL/UserType.cs index f5d1818a94a..c1c09a4de73 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/GraphQL/UserType.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/GraphQL/UserType.cs @@ -3,6 +3,8 @@ using GraphQL.Types; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Localization; +using OrchardCore.ContentManagement; +using OrchardCore.ContentManagement.GraphQL.Queries.Types; using OrchardCore.ContentManagement.Metadata.Models; using OrchardCore.Users.Models; using OrchardCore.Users.Services; @@ -30,27 +32,29 @@ public UserType(IStringLocalizer localizer) internal void AddField(ISchema schema, ContentTypeDefinition typeDefinition) { var contentItemType = schema.AdditionalTypeInstances.SingleOrDefault(t => t.Name == typeDefinition.Name); - + if (contentItemType == null) { // This error would indicate that this graph type is build too early. throw new InvalidOperationException("ContentTypeDefinition has not been registered in GraphQL"); } - var field = Field(typeDefinition.Name, contentItemType.GetType()) - .Description(S["Custom user settings of {0}.", typeDefinition.DisplayName]) - .ResolveAsync(static async context => { - // We don't want to create an empty content item if it does not exist. - if (context.Source is User user && - user.Properties.ContainsKey(context.FieldDefinition.ResolvedType.Name)) - { - var customUserSettingsService = context.RequestServices!.GetRequiredService(); - var settingsType = await customUserSettingsService.GetSettingsTypeAsync(context.FieldDefinition.ResolvedType.Name); - - return await customUserSettingsService.GetSettingsAsync(user, settingsType); - } - - return null; - }); + Field(typeDefinition.Name) + .Type(contentItemType) + .Description(S["Custom user settings of {0}.", typeDefinition.DisplayName]) + .ResolveAsync(static async context => + { + // We don't want to create an empty content item if it does not exist. + if (context.Source is User user && + user.Properties.ContainsKey(context.FieldDefinition.ResolvedType.Name)) + { + var customUserSettingsService = context.RequestServices!.GetRequiredService(); + var settingsType = await customUserSettingsService.GetSettingsTypeAsync(context.FieldDefinition.ResolvedType.Name); + + return await customUserSettingsService.GetSettingsAsync(user, settingsType); + } + + return null; + }); } }