From 9c4a2736f5d588dc05a8b12459496a0d24da025c Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sat, 24 Oct 2020 20:17:54 +0200 Subject: [PATCH] GraphQL reverse references. (#592) * GraphQL reverse references. * Fixes. * Fix recursion. --- .../Contents/MongoContentCollectionAll.cs | 4 +- .../MongoContentCollectionPublished.cs | 4 +- .../Contents/MongoContentRepository.cs | 6 +- .../Operations/QueryContentsByQuery.cs | 11 +- .../Contents/GraphQL/GraphQLModel.cs | 12 +- .../GraphQL/Types/AppQueriesGraphType.cs | 6 +- .../Contents/GraphQL/Types/ContentActions.cs | 18 ++- .../GraphQL/Types/ContentGraphType.cs | 77 +++++++++-- .../Contents/Queries/ContentQueryService.cs | 26 ++-- .../Contents/Queries/QueryExecutionContext.cs | 19 ++- .../Repositories/IContentRepository.cs | 2 +- backend/src/Squidex.Domain.Apps.Entities/Q.cs | 7 + .../Frontend/Middlewares/WebpackMiddleware.cs | 3 - .../Contents/GraphQL/GraphQLQueriesTests.cs | 127 ++++++++++++++++++ .../Contents/MongoDb/ContentsQueryTests.cs | 48 ++++++- .../Queries/ContentQueryServiceTests.cs | 6 +- .../TestHelpers/AExtensions.cs | 10 +- 17 files changed, 336 insertions(+), 50 deletions(-) diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollectionAll.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollectionAll.cs index 76b165123..e86b72373 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollectionAll.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollectionAll.cs @@ -64,11 +64,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents await queryScheduledItems.PrepareAsync(collection, ct); } - public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, ClrQuery query) + public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, ClrQuery query, Guid? referenced) { using (Profiler.TraceMethod("QueryAsyncByQuery")) { - return await queryContentsByQuery.DoAsync(app, schema, query, SearchScope.All); + return await queryContentsByQuery.DoAsync(app, schema, query, referenced, SearchScope.All); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollectionPublished.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollectionPublished.cs index 3b3d13319..89b797168 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollectionPublished.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollectionPublished.cs @@ -65,11 +65,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents await queryIdsAsync.PrepareAsync(collection, ct); } - public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, ClrQuery query) + public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, ClrQuery query, Guid? referenced) { using (Profiler.TraceMethod("QueryAsyncByQuery")) { - return await queryContentsByQuery.DoAsync(app, schema, query, SearchScope.Published); + return await queryContentsByQuery.DoAsync(app, schema, query, referenced, SearchScope.Published); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs index f69b64052..62bae4c52 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs @@ -54,15 +54,15 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents await collectionPublished.InitializeAsync(ct); } - public Task> QueryAsync(IAppEntity app, ISchemaEntity schema, ClrQuery query, SearchScope scope) + public Task> QueryAsync(IAppEntity app, ISchemaEntity schema, ClrQuery query, Guid? referenced, SearchScope scope) { if (scope == SearchScope.All) { - return collectionAll.QueryAsync(app, schema, query); + return collectionAll.QueryAsync(app, schema, query, referenced); } else { - return collectionPublished.QueryAsync(app, schema, query); + return collectionPublished.QueryAsync(app, schema, query, referenced); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryContentsByQuery.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryContentsByQuery.cs index 145d3919b..09f40d15d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryContentsByQuery.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryContentsByQuery.cs @@ -68,7 +68,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations await Collection.Indexes.CreateOneAsync(indexBySchema, cancellationToken: ct); } - public async Task> DoAsync(IAppEntity app, ISchemaEntity schema, ClrQuery query, SearchScope scope) + public async Task> DoAsync(IAppEntity app, ISchemaEntity schema, ClrQuery query, Guid? referenced, SearchScope scope) { Guard.NotNull(app, nameof(app)); Guard.NotNull(schema, nameof(schema)); @@ -92,7 +92,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations } } - var filter = CreateFilter(schema.Id, fullTextIds, query); + var filter = CreateFilter(schema.Id, fullTextIds, query, referenced); var contentCount = Collection.Find(filter).CountDocumentsAsync(); var contentItems = FindContentsAsync(query, filter); @@ -155,7 +155,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations return query.Sort?.All(x => x.Path.ToString() == "mt" && x.Order == SortOrder.Descending) == true; } - private static FilterDefinition CreateFilter(Guid schemaId, ICollection? ids, ClrQuery? query) + private static FilterDefinition CreateFilter(Guid schemaId, ICollection? ids, ClrQuery? query, Guid? referenced) { var filters = new List> { @@ -176,6 +176,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations filters.Add(query.Filter.BuildFilter()); } + if (referenced != null) + { + filters.Add(Filter.AnyEq(x => x.ReferencedIds, referenced.Value)); + } + return Filter.And(filters); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs index 54fff83b8..b05a40552 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs @@ -56,7 +56,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL graphQLSchema.RegisterValueConverter(JsonConverter.Instance); graphQLSchema.RegisterValueConverter(InstantConverter.Instance); - InitializeContentTypes(); + InitializeContentTypes(allSchemas, pageSizeContents); } private void BuildSchemas(List allSchemas) @@ -67,11 +67,17 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } } - private void InitializeContentTypes() + private void InitializeContentTypes(List allSchemas, int pageSize) { + var i = 0; + foreach (var contentType in contentTypes.Values) { - contentType.Initialize(this); + var schema = allSchemas[i]; + + contentType.Initialize(this, schema, allSchemas, pageSize); + + i++; } foreach (var contentType in contentTypes.Values) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs index 517de2bc1..e8b70710b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs @@ -85,12 +85,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types private void AddContentQueries(Guid schemaId, string schemaType, string schemaName, IGraphType contentType, int pageSize) { - var resolver = ContentActions.Query.Resolver(schemaId); + var resolver = ContentActions.QueryOrReferencing.Query(schemaId); AddField(new FieldType { Name = $"query{schemaType}Contents", - Arguments = ContentActions.Query.Arguments(pageSize), + Arguments = ContentActions.QueryOrReferencing.Arguments(pageSize), ResolvedType = new ListGraphType(new NonNullGraphType(contentType)), Resolver = resolver, Description = $"Query {schemaName} content items." @@ -99,7 +99,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types AddField(new FieldType { Name = $"query{schemaType}ContentsWithTotal", - Arguments = ContentActions.Query.Arguments(pageSize), + Arguments = ContentActions.QueryOrReferencing.Arguments(pageSize), ResolvedType = new ContentsResultGraphType(schemaType, schemaName, contentType), Resolver = resolver, Description = $"Query {schemaName} content items with total count." diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentActions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentActions.cs index 2c5a2b763..a7a65e244 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentActions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentActions.cs @@ -82,7 +82,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types }); } - public static class Query + public static class QueryOrReferencing { private static QueryArguments? arguments; @@ -128,7 +128,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types }; } - public static IFieldResolver Resolver(Guid schemaId) + public static IFieldResolver Query(Guid schemaId) { var schemaIdValue = schemaId.ToString(); @@ -139,6 +139,20 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types return ((GraphQLExecutionContext)c.UserContext).QueryContentsAsync(schemaIdValue, query); }); } + + public static IFieldResolver Referencing(Guid schemaId) + { + var schemaIdValue = schemaId.ToString(); + + return new FuncFieldResolver(c => + { + var query = c.BuildODataQuery(); + + var contentId = c.Source.Id; + + return ((GraphQLExecutionContext)c.UserContext).QueryReferencingContentsAsync(schemaIdValue, query, c.Source.Id); + }); + } } public static class Create diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs index 05ba7fb4b..514dc40f1 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs @@ -5,24 +5,25 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; +using System.Collections.Generic; using System.Linq; using GraphQL.Types; +using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Entities.Schemas; namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { public sealed class ContentGraphType : ObjectGraphType { - private readonly ISchemaEntity schema; - private readonly string schemaType; - private readonly string schemaName; + private readonly Guid schemaId; public ContentGraphType(ISchemaEntity schema) { - this.schema = schema; + this.schemaId = schema.Id; - schemaType = schema.TypeName(); - schemaName = schema.DisplayName(); + var schemaType = schema.TypeName(); + var schemaName = schema.DisplayName(); Name = $"{schemaType}"; @@ -99,11 +100,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types private bool CheckType(object value) { - return value is IContentEntity content && content.SchemaId?.Id == schema.Id; + return value is IContentEntity content && content.SchemaId?.Id == schemaId; } - public void Initialize(IGraphModel model) + public void Initialize(IGraphModel model, ISchemaEntity schema, IEnumerable all, int pageSize) { + var schemaType = schema.TypeName(); + var schemaName = schema.DisplayName(); + AddField(new FieldType { Name = "url", @@ -137,6 +141,63 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types Description = $"The flat data of the {schemaName} content." }); } + + foreach (var other in all.Where(x => References(x, schema))) + { + var referencingId = other.Id; + var referencingType = other.TypeName(); + var referencingName = other.DisplayName(); + + var contentType = model.GetContentType(referencingId); + + AddReferencingQueries(referencingId, referencingType, referencingName, contentType, pageSize); + } + } + + private void AddReferencingQueries(Guid referencingId, string referencingType, string referencingName, IGraphType contentType, int pageSize) + { + var resolver = ContentActions.QueryOrReferencing.Referencing(referencingId); + + AddField(new FieldType + { + Name = $"referencing{referencingType}Contents", + Arguments = ContentActions.QueryOrReferencing.Arguments(pageSize), + ResolvedType = new ListGraphType(new NonNullGraphType(contentType)), + Resolver = resolver, + Description = $"Query {referencingName} content items." + }); + + AddField(new FieldType + { + Name = $"referencing{referencingType}ContentsWithTotal", + Arguments = ContentActions.QueryOrReferencing.Arguments(pageSize), + ResolvedType = new ContentsResultGraphType(referencingType, referencingName, contentType), + Resolver = resolver, + Description = $"Query {referencingName} content items with total count." + }); + } + + private static bool References(ISchemaEntity other, ISchemaEntity schema) + { + var id = schema.Id; + + return other.SchemaDef.Fields.Any(x => References(x, id)); + } + + private static bool References(IField field, Guid id) + { + switch (field) + { + case IField reference: + return + reference.Properties.SchemaIds == null || + reference.Properties.SchemaIds.Count == 0 || + reference.Properties.SchemaIds.Contains(id); + case IArrayField arrayField: + return arrayField.Fields.Any(x => References(x, id)); + } + + return false; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs index 9836c1ea5..149e1b01d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs @@ -55,6 +55,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries { Guard.NotNull(context, nameof(context)); + if (id == default) + { + throw new DomainObjectNotFoundException(id.ToString()); + } + var schema = await GetSchemaOrThrowAsync(context, schemaIdOrName); CheckPermission(context, schema); @@ -85,6 +90,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries { Guard.NotNull(context, nameof(context)); + if (query == null) + { + return EmptyContents; + } + var schema = await GetSchemaOrThrowAsync(context, schemaIdOrName); CheckPermission(context, schema); @@ -110,13 +120,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries { Guard.NotNull(context, nameof(context)); - using (Profiler.TraceMethod()) + if (ids == null || ids.Count == 0) { - if (ids == null || ids.Count == 0) - { - return EmptyContents; - } + return EmptyContents; + } + using (Profiler.TraceMethod()) + { var contents = await QueryCoreAsync(context, ids); var filtered = @@ -213,7 +223,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries { var parsedQuery = await queryParser.ParseQueryAsync(context, schema, query); - return await QueryCoreAsync(context, schema, parsedQuery); + return await QueryCoreAsync(context, schema, parsedQuery, query.Reference); } private async Task> QueryByIdsAsync(Context context, ISchemaEntity schema, Q query) @@ -228,9 +238,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries return contentRepository.QueryAsync(context.App, new HashSet(ids), context.Scope()); } - private Task> QueryCoreAsync(Context context, ISchemaEntity schema, ClrQuery query) + private Task> QueryCoreAsync(Context context, ISchemaEntity schema, ClrQuery query, Guid? referenced) { - return contentRepository.QueryAsync(context.App, schema, query, context.Scope()); + return contentRepository.QueryAsync(context.App, schema, query, referenced, context.Scope()); } private Task> QueryCoreAsync(Context context, ISchemaEntity schema, HashSet ids) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs index 1491334bb..866e49213 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs @@ -73,9 +73,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries return content; } - public virtual async Task> QueryAssetsAsync(string query) + public virtual async Task> QueryAssetsAsync(string odata) { - var assets = await assetQuery.QueryAsync(context, null, Q.Empty.WithODataQuery(query)); + var q = Q.Empty.WithODataQuery(odata); + + var assets = await assetQuery.QueryAsync(context, null, q); foreach (var asset in assets) { @@ -85,9 +87,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries return assets; } - public virtual async Task> QueryContentsAsync(string schemaIdOrName, string query) + public virtual async Task> QueryContentsAsync(string schemaIdOrName, string odata) { - var result = await contentQuery.QueryAsync(context, schemaIdOrName, Q.Empty.WithODataQuery(query)); + var q = Q.Empty.WithODataQuery(odata); + + var result = await contentQuery.QueryAsync(context, schemaIdOrName, q); foreach (var content in result) { @@ -134,5 +138,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries return ids.Select(cachedContents.GetOrDefault).NotNull().ToList(); } + + public Task> QueryReferencingContentsAsync(string schemaIdOrName, string odata, Guid reference) + { + var q = Q.Empty.WithODataQuery(odata).WithReference(reference); + + return contentQuery.QueryAsync(context, schemaIdOrName, q); + } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs index e0f8ff9b9..55a4bc89e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs @@ -22,7 +22,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Repositories Task> QueryAsync(IAppEntity app, ISchemaEntity schema, HashSet ids, SearchScope scope); - Task> QueryAsync(IAppEntity app, ISchemaEntity schema, ClrQuery query, SearchScope scope); + Task> QueryAsync(IAppEntity app, ISchemaEntity schema, ClrQuery query, Guid? referenced, SearchScope scope); Task> QueryIdsAsync(Guid appId, Guid schemaId, FilterNode filterNode); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Q.cs b/backend/src/Squidex.Domain.Apps.Entities/Q.cs index 6737a3c84..7fc94ee55 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Q.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Q.cs @@ -20,6 +20,8 @@ namespace Squidex.Domain.Apps.Entities public IReadOnlyList Ids { get; private set; } + public Guid? Reference { get; private set; } + public string? ODataQuery { get; private set; } public string? JsonQuery { get; private set; } @@ -53,6 +55,11 @@ namespace Squidex.Domain.Apps.Entities return Clone(c => c.Ids = ids.ToList()); } + public Q WithReference(Guid? reference) + { + return Clone(c => c.Reference = reference); + } + public Q WithIds(IEnumerable ids) { return Clone(c => c.Ids = ids.ToList()); diff --git a/backend/src/Squidex/Areas/Frontend/Middlewares/WebpackMiddleware.cs b/backend/src/Squidex/Areas/Frontend/Middlewares/WebpackMiddleware.cs index 694167c1f..64cd52412 100644 --- a/backend/src/Squidex/Areas/Frontend/Middlewares/WebpackMiddleware.cs +++ b/backend/src/Squidex/Areas/Frontend/Middlewares/WebpackMiddleware.cs @@ -5,10 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.IO; -using System.Net; using System.Net.Http; -using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; 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 5f9256f03..e1329a5c5 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 @@ -460,6 +460,133 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL AssertResult(expected, result); } + [Fact] + public async Task Should_also_fetch_referencing_contents_when_field_is_included_in_query() + { + var contentRefId = Guid.NewGuid(); + var contentRef = TestContent.CreateRef(schemaRefId1, contentRefId, "ref1-field", "ref1"); + + var contentId = Guid.NewGuid(); + var content = TestContent.Create(schemaId, contentId, contentRefId, Guid.Empty); + + var query = @" + query { + findMyRefSchema1Content(id: """") { + id + referencingMySchemaContents(top: 30, skip: 5) { + id + data { + myString { + de + } + } + } + } + }".Replace("", contentRefId.ToString()); + + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A>._)) + .Returns(ResultList.CreateFrom(0, contentRef)); + + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.Id.ToString(), A.That.HasOData("?$top=30&$skip=5", contentRefId))) + .Returns(ResultList.CreateFrom(1, content)); + + var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + findMyRefSchema1Content = new + { + id = contentRefId, + referencingMySchemaContents = new[] + { + new + { + id = contentId, + data = new + { + myString = new + { + de = "value" + } + } + } + } + } + } + }; + + AssertResult(expected, result); + } + + [Fact] + public async Task Should_also_fetch_referencing_contents_with_total_when_field_is_included_in_query() + { + var contentRefId = Guid.NewGuid(); + var contentRef = TestContent.CreateRef(schemaRefId1, contentRefId, "ref1-field", "ref1"); + + var contentId = Guid.NewGuid(); + var content = TestContent.Create(schemaId, contentId, contentRefId, Guid.Empty); + + var query = @" + query { + findMyRefSchema1Content(id: """") { + id + referencingMySchemaContentsWithTotal(top: 30, skip: 5) { + total + items { + id + data { + myString { + de + } + } + } + } + } + }".Replace("", contentRefId.ToString()); + + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A>._)) + .Returns(ResultList.CreateFrom(0, contentRef)); + + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.Id.ToString(), A.That.HasOData("?$top=30&$skip=5", contentRefId))) + .Returns(ResultList.CreateFrom(1, content)); + + var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + findMyRefSchema1Content = new + { + id = contentRefId, + referencingMySchemaContentsWithTotal = new + { + total = 1, + items = new[] + { + new + { + id = contentId, + data = new + { + myString = new + { + de = "value" + } + } + } + } + } + } + } + }; + + AssertResult(expected, result); + } + [Fact] public async Task Should_also_fetch_union_contents_when_field_is_included_in_query() { diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryTests.cs index abb9142a3..5caa6a449 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryTests.cs @@ -60,7 +60,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb } [Fact] - public async Task Should_query_contents_by_filter() + public async Task Should_query_contents_ids_by_filter() { var filter = F.Eq("data.value.iv", 12); @@ -69,6 +69,23 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb Assert.NotEmpty(contents); } + [Fact] + public async Task Should_query_contents_by_filter() + { + var query = new ClrQuery + { + Sort = new List + { + new SortNode("lastModified", SortOrder.Descending) + }, + Filter = F.Eq("data.value.iv", 12) + }; + + var contents = await _.ContentRepository.QueryAsync(_.RandomApp(), _.RandomSchema(), query, null, SearchScope.Published); + + Assert.NotEmpty(contents); + } + [Fact] public async Task Should_query_contents_scheduled() { @@ -78,7 +95,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb } [Fact] - public async Task Should_query_contents_by_default() + public async Task Should_query_contents_with_default_query() { var query = new ClrQuery(); @@ -87,6 +104,16 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb Assert.NotEmpty(contents); } + [Fact] + public async Task Should_query_contents_with_default_query_and_id() + { + var query = new ClrQuery(); + + var contents = await QueryAsync(query, id: Guid.NewGuid()); + + Assert.NotEmpty(contents); + } + [Fact] public async Task Should_query_contents_with_large_skip() { @@ -129,7 +156,20 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb Assert.NotEmpty(contents); } - private async Task> QueryAsync(ClrQuery clrQuery, int take = 1000, int skip = 100) + [Fact] + public async Task Should_query_contents_with_query_filter_and_id() + { + var query = new ClrQuery + { + Filter = F.Eq("data.value.iv", 12) + }; + + var contents = await QueryAsync(query, 1000, 0, id: Guid.NewGuid()); + + Assert.Empty(contents); + } + + private async Task> QueryAsync(ClrQuery clrQuery, int take = 1000, int skip = 100, Guid? id = null) { if (clrQuery.Take == long.MaxValue) { @@ -149,7 +189,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb }; } - var contents = await _.ContentRepository.QueryAsync(_.RandomApp(), _.RandomSchema(), clrQuery, SearchScope.All); + var contents = await _.ContentRepository.QueryAsync(_.RandomApp(), _.RandomSchema(), clrQuery, id, SearchScope.All); return contents; } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs index af74c5bf9..7e27db8ee 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs @@ -183,16 +183,18 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries [InlineData(0, 0, SearchScope.Published)] public async Task QueryAsync_should_return_contents(int isFrontend, int unpublished, SearchScope scope) { + var reference = Guid.NewGuid(); + var ctx = CreateContext(isFrontend: isFrontend == 1, allowSchema: true) .WithUnpublished(unpublished == 1); var content = CreateContent(contentId); - A.CallTo(() => contentRepository.QueryAsync(ctx.App, schema, A._, scope)) + A.CallTo(() => contentRepository.QueryAsync(ctx.App, schema, A._, reference, scope)) .Returns(ResultList.CreateFrom(5, content)); - var result = await sut.QueryAsync(ctx, schemaId.Name, Q.Empty); + var result = await sut.QueryAsync(ctx, schemaId.Name, Q.Empty.WithReference(reference)); Assert.Equal(contentData, result[0].Data); Assert.Equal(contentId, result[0].Id); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AExtensions.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AExtensions.cs index 429b4475f..419a41cbb 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AExtensions.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AExtensions.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Collections.Generic; using System.Linq; using FakeItEasy; @@ -19,9 +20,14 @@ namespace Squidex.Domain.Apps.Entities.TestHelpers return that.Matches(x => x.Query!.ToString() == query); } - public static Q HasOData(this INegatableArgumentConstraintManager that, string query) + public static Q HasOData(this INegatableArgumentConstraintManager that, string odata) { - return that.Matches(x => x.ODataQuery == query); + return that.HasOData(odata, null); + } + + public static Q HasOData(this INegatableArgumentConstraintManager that, string odata, Guid? reference = null) + { + return that.Matches(x => x.ODataQuery == odata && x.Reference == reference); } public static ClrQuery Is(this INegatableArgumentConstraintManager that, string query)