diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Builder.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Builder.cs index abc04838b..87ba1f404 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Builder.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Builder.cs @@ -15,6 +15,7 @@ using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents; using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Dynamic; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; +using Squidex.Infrastructure.Collections; using GraphQLSchema = GraphQL.Types.Schema; namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types @@ -25,6 +26,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types private readonly Dictionary contentTypes = new Dictionary(ReferenceEqualityComparer.Instance); private readonly Dictionary contentResultTypes = new Dictionary(ReferenceEqualityComparer.Instance); private readonly Dictionary embeddableStringTypes = new Dictionary(); + private readonly Dictionary referenceUnionTypes = new Dictionary(); + private readonly Dictionary componentUnionTypes = new Dictionary(); + private readonly Dictionary nestedTypes = new Dictionary(); private readonly Dictionary enumTypes = new Dictionary(); private readonly Dictionary dynamicTypes = new Dictionary(); private readonly FieldVisitor fieldVisitor; @@ -192,6 +196,21 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types return enumTypes.GetOrAdd(name, x => FieldEnumType.TryCreate(name, values)); } + public ReferenceUnionGraphType GetReferenceUnion(FieldInfo fieldInfo, ReadonlyList? schemaIds) + { + return referenceUnionTypes.GetOrAdd(fieldInfo, x => new ReferenceUnionGraphType(this, x, schemaIds)); + } + + public ComponentUnionGraphType GetComponentUnion(FieldInfo fieldInfo, ReadonlyList? schemaIds) + { + return componentUnionTypes.GetOrAdd(fieldInfo, x => new ComponentUnionGraphType(this, x, schemaIds)); + } + + public NestedGraphType GetNested(FieldInfo fieldInfo) + { + return nestedTypes.GetOrAdd(fieldInfo, x => new NestedGraphType(this, x)); + } + public IEnumerable> GetAllContentTypes() { return contentTypes; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ComponentUnionGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ComponentUnionGraphType.cs index d900d58d9..d3a34d80e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ComponentUnionGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ComponentUnionGraphType.cs @@ -22,7 +22,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents public ComponentUnionGraphType(Builder builder, FieldInfo fieldInfo, ReadonlyList? schemaIds) { // The name is used for equal comparison. Therefore it is important to treat it as readonly. - Name = fieldInfo.UnionReferenceType; + Name = fieldInfo.UnionComponentType; if (schemaIds?.Any() == true) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/EmbeddableStringGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/EmbeddableStringGraphType.cs index 20d01a75a..cf785f2ce 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/EmbeddableStringGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/EmbeddableStringGraphType.cs @@ -48,7 +48,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents if (contentType == null) { - var union = new ReferenceUnionGraphType(builder, fieldInfo, schemaIds); + var union = builder.GetReferenceUnion(fieldInfo, schemaIds); if (!union.HasType) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldVisitor.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldVisitor.cs index f47ab8b5d..5c8c80eb9 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldVisitor.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldVisitor.cs @@ -126,7 +126,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents return default; } - var type = new NestedGraphType(builder, args); + var type = builder.GetNested(args); if (type.Fields.Count == 0) { @@ -263,7 +263,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents if (contentType == null) { - var union = new ReferenceUnionGraphType(builder, fieldInfo, schemaIds); + var union = builder.GetReferenceUnion(fieldInfo, schemaIds); if (!union.HasType) { @@ -287,7 +287,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents if (componentType == null) { - var union = new ComponentUnionGraphType(builder, fieldInfo, schemaIds); + var union = builder.GetComponentUnion(fieldInfo, schemaIds); if (!union.HasType) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/SharedExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/SharedExtensions.cs index b0afe2ba3..126ffff4e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/SharedExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/SharedExtensions.cs @@ -32,6 +32,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types if (!string.IsNullOrWhiteSpace(formatted)) { + if (key == "search") + { + formatted = $"\"{formatted.Trim('"')}\""; + } + if (sb.Length > 1) { sb.Append('&'); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj b/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj index 27d2888f5..ae8cd96e5 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj +++ b/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj @@ -20,8 +20,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/backend/src/Squidex.Web/GraphQL/GraphQLRunner.cs b/backend/src/Squidex.Web/GraphQL/GraphQLRunner.cs index 6e76e176f..2f9d79989 100644 --- a/backend/src/Squidex.Web/GraphQL/GraphQLRunner.cs +++ b/backend/src/Squidex.Web/GraphQL/GraphQLRunner.cs @@ -8,6 +8,7 @@ using GraphQL.Server.Transports.AspNetCore; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Net.Http.Headers; namespace Squidex.Web.GraphQL { @@ -19,7 +20,10 @@ namespace Squidex.Web.GraphQL { RequestDelegate next = x => Task.CompletedTask; - var options = new GraphQLHttpMiddlewareOptions(); + var options = new GraphQLHttpMiddlewareOptions + { + DefaultResponseContentType = new MediaTypeHeaderValue("application/json") + }; middleware = ActivatorUtilities.CreateInstance>(serviceProvider, next, options); } diff --git a/backend/src/Squidex.Web/Squidex.Web.csproj b/backend/src/Squidex.Web/Squidex.Web.csproj index 998fe87b5..3bda29ec0 100644 --- a/backend/src/Squidex.Web/Squidex.Web.csproj +++ b/backend/src/Squidex.Web/Squidex.Web.csproj @@ -13,9 +13,9 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs index f219b2fdd..c0acda5d5 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs @@ -24,18 +24,17 @@ namespace Squidex.Areas.Api.Controllers.Contents { private readonly IContentQueryService contentQuery; private readonly IContentWorkflow contentWorkflow; - private readonly GraphQLRunner graphQLMiddleware; + private readonly GraphQLRunner graphQLRunner; public ContentsController(ICommandBus commandBus, IContentQueryService contentQuery, IContentWorkflow contentWorkflow, - GraphQLRunner graphQLMiddleware) + GraphQLRunner graphQLRunner) : base(commandBus) { this.contentQuery = contentQuery; this.contentWorkflow = contentWorkflow; - - this.graphQLMiddleware = graphQLMiddleware; + this.graphQLRunner = graphQLRunner; } /// @@ -57,7 +56,7 @@ namespace Squidex.Areas.Api.Controllers.Contents [ApiCosts(2)] public Task GetGraphQL(string app) { - return graphQLMiddleware.InvokeAsync(HttpContext); + return graphQLRunner.InvokeAsync(HttpContext); } /// diff --git a/backend/src/Squidex/Squidex.csproj b/backend/src/Squidex/Squidex.csproj index 035d64168..6415feda3 100644 --- a/backend/src/Squidex/Squidex.csproj +++ b/backend/src/Squidex/Squidex.csproj @@ -35,9 +35,9 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs index b01bdc17b..34ec8efe3 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs @@ -46,6 +46,39 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL AssertResult(expected, actual); } + [Fact] + public async Task Should_query_contents_with_full_text() + { + var query = CreateQuery(@" + query { + queryMySchemaContents(search: ""Hello"") { + + } + }"); + + var contentId = DomainId.NewGuid(); + var content = TestContent.Create(contentId); + + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), TestSchemas.Default.Id.ToString(), + A.That.Matches(x => x.QueryAsOdata == "?$skip=0&$search=\"Hello\"" && x.NoTotal), A._)) + .Returns(ResultList.CreateFrom(0, content)); + + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); + + var expected = new + { + data = new + { + queryMySchemaContents = new[] + { + TestContent.FlatResponse(content) + } + } + }; + + AssertResult(expected, actual); + } + [Fact] public async Task Should_return_multiple_assets_if_querying_assets() { diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj index 460bf30ce..e32946928 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj @@ -23,8 +23,8 @@ - - + + all diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs index 6466fd046..dca465dc3 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs @@ -8,6 +8,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Squidex.ClientLibrary; +using Squidex.ClientLibrary.Utils; using TestSuite.Model; #pragma warning disable SA1300 // Element should begin with upper-case letter @@ -627,6 +628,65 @@ namespace TestSuite.ApiTests Assert.Equal(items.Select(x => x["data"]["number"]["iv"].Value()).ToArray(), new[] { 4, 5, 6 }); } + [Fact] + public async Task Should_query_items_complex_search() + { + var query = new + { + query = @" + query ContentsQuery($search: String!) { + queryMyReadsContents(search: $search) { + id, + data { + number { + iv + } + } + } + }", + variables = new + { + search = @"The answer is 42" + } + }; + + await _.Contents.GraphQlAsync(query); + } + + [Fact] + public async Task Should_return_correct_content_type_for_graphql() + { + var query = new + { + query = @" + query ContentsQuery($filter: String!) { + queryMyReadsContents(filter: $filter, orderby: ""data/number/iv asc"") { + id, + data { + number { + iv + } + } + } + }", + variables = new + { + filter = @"data/number/iv gt 3 and data/number/iv lt 7" + } + }; + + using (var client = _.ClientManager.CreateHttpClient()) + { + var response = await client.PostAsync(_.ClientManager.GenerateUrl($"api/content/{_.AppName}/graphql/batch"), query.ToContent()); + + Assert.Equal("application/json", response.Content.Headers.ContentType.MediaType); + + var result = await response.Content.ReadAsJsonAsync>(); + + Assert.Equal(result.Data.Items.Select(x => x.Data.Number).ToArray(), new[] { 4, 5, 6 }); + } + } + private sealed class QueryResult { [JsonProperty("queryMyReadsContents")]