diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.Designer.cs b/backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.Designer.cs index 5504b9fcc..abb9aa2f3 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.Designer.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.Designer.cs @@ -672,6 +672,15 @@ namespace Squidex.Domain.Apps.Core { } } + /// + /// Looks up a localized string similar to The list of IDs.. + /// + public static string EntityIds { + get { + return ResourceManager.GetString("EntityIds", resourceCulture); + } + } + /// /// Looks up a localized string similar to True when deleted.. /// diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.resx b/backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.resx index 102999c0b..fb2bdafa3 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.resx +++ b/backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.resx @@ -321,6 +321,9 @@ The ID of the object (usually GUID). + + The list of IDs. + True when deleted. diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs index 76ebc54dc..ae32af775 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs @@ -154,7 +154,7 @@ public sealed class GraphQLExecutionContext : QueryExecutionContext return dataLoaders.Context!.GetOrAddBatchLoader(nameof(GetAssetsLoader), async (batch, ct) => { - var result = await GetReferencedAssetsAsync(new List(batch), ct); + var result = await QueryAssetsByIdsAsync(new List(batch), ct); return result.ToDictionary(x => x.Id); }); @@ -165,7 +165,7 @@ public sealed class GraphQLExecutionContext : QueryExecutionContext return dataLoaders.Context!.GetOrAddBatchLoader(nameof(GetContentsLoader), async (batch, ct) => { - var result = await GetReferencedContentsAsync(new List(batch), ct); + var result = await QueryContentsByIdsAsync(new List(batch), ct); return result.ToDictionary(x => x.Id); }); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ApplicationQueries.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ApplicationQueries.cs index 3203cb3f9..6b418d1e4 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ApplicationQueries.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ApplicationQueries.cs @@ -17,6 +17,7 @@ internal sealed class ApplicationQueries : ObjectGraphType AddField(SharedTypes.FindAsset); AddField(SharedTypes.QueryAssets); AddField(SharedTypes.QueryAssetsWithTotal); + AddContentQuery(builder); foreach (var schemaInfo in schemaInfos) { @@ -73,4 +74,23 @@ internal sealed class ApplicationQueries : ObjectGraphType Description = $"Query {schemaInfo.DisplayName} content items with total count." }).WithSchemaId(schemaInfo); } + + private void AddContentQuery(Builder builder) + { + var unionType = builder.GetContentUnion("AllContents", null); + + if (!unionType.HasType) + { + return; + } + + AddField(new FieldType + { + Name = "queryContents", + Arguments = ContentActions.QueryByIds.Arguments, + ResolvedType = new NonNullGraphType(new ListGraphType(new NonNullGraphType(unionType))), + Resolver = ContentActions.QueryByIds.Resolver, + Description = "Query content items by IDs across schemeas." + }); + } } 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 217e4a800..4e4f51e18 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 @@ -22,15 +22,15 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types; internal sealed class Builder { - private readonly Dictionary componentTypes = new Dictionary(ReferenceEqualityComparer.Instance); - 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 Dictionary componentUnionTypes = new (); + private readonly Dictionary embeddableStringTypes = new (); + private readonly Dictionary nestedTypes = new (); + private readonly Dictionary componentTypes = new (); + private readonly Dictionary contentTypes = new (); + private readonly Dictionary contentResultTypes = new (); + private readonly Dictionary unionTypes = new (); + private readonly Dictionary enumTypes = new (); + private readonly Dictionary dynamicTypes = new (); private readonly FieldVisitor fieldVisitor; private readonly FieldInputVisitor fieldInputVisitor; private readonly PartitionResolver partitionResolver; @@ -186,19 +186,14 @@ internal sealed class Builder return dynamicTypes.GetOrAdd(graphQLSchema, x => DynamicSchemaBuilder.ParseTypes(x, typeNames)); } - public EmbeddableStringGraphType GetEmbeddableString(FieldInfo fieldInfo, StringFieldProperties properties) - { - return embeddableStringTypes.GetOrAdd(fieldInfo, x => new EmbeddableStringGraphType(this, x, properties)); - } - public EnumerationGraphType? GetEnumeration(string name, IEnumerable values) { return enumTypes.GetOrAdd(name, x => FieldEnumType.TryCreate(name, values)); } - public ReferenceUnionGraphType GetReferenceUnion(FieldInfo fieldInfo, ReadonlyList? schemaIds) + public EmbeddableStringGraphType GetEmbeddableString(FieldInfo fieldInfo, StringFieldProperties properties) { - return referenceUnionTypes.GetOrAdd(fieldInfo, x => new ReferenceUnionGraphType(this, x, schemaIds)); + return embeddableStringTypes.GetOrAdd(fieldInfo, x => new EmbeddableStringGraphType(this, x, properties)); } public ComponentUnionGraphType GetComponentUnion(FieldInfo fieldInfo, ReadonlyList? schemaIds) @@ -206,6 +201,11 @@ internal sealed class Builder return componentUnionTypes.GetOrAdd(fieldInfo, x => new ComponentUnionGraphType(this, x, schemaIds)); } + public ContentUnionGraphType GetContentUnion(string name, ReadonlyList? schemaIds) + { + return unionTypes.GetOrAdd(name, x => new ContentUnionGraphType(this, x, schemaIds)); + } + public NestedGraphType GetNested(FieldInfo fieldInfo) { return nestedTypes.GetOrAdd(fieldInfo, x => new NestedGraphType(this, x)); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs index b8a034284..04905ade6 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs @@ -96,6 +96,26 @@ internal static class ContentActions }); } + public static class QueryByIds + { + public static readonly QueryArguments Arguments = new QueryArguments + { + new QueryArgument(Scalars.Strings) + { + Name = "ids", + Description = FieldDescriptions.EntityIds, + DefaultValue = null + } + }; + + public static readonly IFieldResolver Resolver = Resolvers.Async(async (_, fieldContext, context) => + { + var contentIds = fieldContext.GetArgument("ids"); + + return await context.QueryContentsByIdsAsync(contentIds, fieldContext.CancellationToken); + }); + } + public static class QueryOrReferencing { public static readonly QueryArguments Arguments = new QueryArguments diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentFields.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentFields.cs index 72725f412..4036f9c0e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentFields.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentFields.cs @@ -155,6 +155,14 @@ internal static class ContentFields Description = FieldDescriptions.EditToken }; + public static readonly FieldType DataDynamic = new FieldType + { + Name = "data__dynamic", + ResolvedType = Scalars.JsonNoop, + Resolver = Resolve(x => x.Data), + Description = FieldDescriptions.ContentData + }; + public static readonly FieldType StringFieldText = new FieldType { Name = "text", diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentGraphType.cs index 5fff6c4e1..adc96ec0b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentGraphType.cs @@ -42,6 +42,7 @@ internal sealed class ContentGraphType : ObjectGraphType AddField(ContentFields.StatusColor); AddField(ContentFields.NewStatus); AddField(ContentFields.NewStatusColor); + AddField(ContentFields.DataDynamic); AddField(ContentFields.Url); var contentDataType = new DataGraphType(builder, schemaInfo); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentInterfaceGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentInterfaceGraphType.cs index 90c417a29..21df91da5 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentInterfaceGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentInterfaceGraphType.cs @@ -26,6 +26,7 @@ internal sealed class ContentInterfaceGraphType : InterfaceGraphType types = new Dictionary(); public bool HasType => types.Count > 0; - public ReferenceUnionGraphType(Builder builder, FieldInfo fieldInfo, ReadonlyList? schemaIds) + public ContentUnionGraphType(Builder builder, string name, ReadonlyList? schemaIds) { // The name is used for equal comparison. Therefore it is important to treat it as readonly. - Name = fieldInfo.UnionReferenceType; + Name = name; 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 a80388f04..9c9c0d828 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 @@ internal sealed class EmbeddableStringGraphType : ObjectGraphType if (contentType == null) { - var union = builder.GetReferenceUnion(fieldInfo, schemaIds); + var union = builder.GetContentUnion(fieldInfo.UnionReferenceType, 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 33f5a3ea8..54601c211 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 @@ -263,7 +263,7 @@ internal sealed class FieldVisitor : IFieldVisitor if (contentType == null) { - var union = builder.GetReferenceUnion(fieldInfo, schemaIds); + var union = builder.GetContentUnion(fieldInfo.UnionReferenceType, schemaIds); if (!union.HasType) { 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 b60f2bc0b..2ac080eca 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs @@ -90,7 +90,7 @@ public abstract class QueryExecutionContext : Dictionary return contents; } - public virtual async Task> GetReferencedAssetsAsync(IEnumerable ids, + public virtual async Task> QueryAssetsByIdsAsync(IEnumerable ids, CancellationToken ct) { Guard.NotNull(ids); @@ -111,7 +111,7 @@ public abstract class QueryExecutionContext : Dictionary }); } - public virtual async Task> GetReferencedContentsAsync(IEnumerable ids, + public virtual async Task> QueryContentsByIdsAsync(IEnumerable ids, CancellationToken ct) { Guard.NotNull(ids); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLIntrospectionTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLIntrospectionTests.cs index 576dfcf08..4407ebfe2 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLIntrospectionTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLIntrospectionTests.cs @@ -117,7 +117,7 @@ public class GraphQLIntrospectionTests : GraphQLTestBase } [Fact] - public async Task Should_create_empty_schema() + public async Task Should_build_graphql_schema_without_schemas() { var model = await CreateSut().GetSchemaAsync(TestApp.Default); @@ -125,7 +125,7 @@ public class GraphQLIntrospectionTests : GraphQLTestBase } [Fact] - public async Task Should_create_empty_schema_with_empty_schema() + public async Task Should_build_graphql_schema_without_schema() { var schema = Mocks.Schema(TestApp.DefaultId, @@ -138,7 +138,7 @@ public class GraphQLIntrospectionTests : GraphQLTestBase } [Fact] - public async Task Should_create_empty_schema_with_empty_schema_because_ui_field() + public async Task Should_build_graphql_schema_on_schema_with_ui_fields_only() { var schema = Mocks.Schema(TestApp.DefaultId, @@ -152,7 +152,7 @@ public class GraphQLIntrospectionTests : GraphQLTestBase } [Fact] - public async Task Should_create_empty_schema_with_empty_schema_because_invalid_field() + public async Task Should_build_graphql_schema_on_schema_with_invalid_fields_only() { var schema = Mocks.Schema(TestApp.DefaultId, @@ -166,7 +166,7 @@ public class GraphQLIntrospectionTests : GraphQLTestBase } [Fact] - public async Task Should_create_empty_schema_with_unpublished_schema() + public async Task Should_build_graphql_schema_on_unpublished_schema() { var schema = Mocks.Schema(TestApp.DefaultId, @@ -180,7 +180,7 @@ public class GraphQLIntrospectionTests : GraphQLTestBase } [Fact] - public async Task Should_create_schema_with_reserved_schema_name() + public async Task Should_build_graphql_schema_on_reserved_schema_name() { var schema = Mocks.Schema(TestApp.DefaultId, @@ -194,7 +194,7 @@ public class GraphQLIntrospectionTests : GraphQLTestBase } [Fact] - public async Task Should_create_schema_with_reserved_field_name() + public async Task Should_build_graphql_schema_with_reserved_field_name() { var schema = Mocks.Schema(TestApp.DefaultId, @@ -210,7 +210,7 @@ public class GraphQLIntrospectionTests : GraphQLTestBase } [Fact] - public async Task Should_create_schema_with_invalid_field_name() + public async Task Should_build_graphql_schema_on_invalid_field_name() { var schema = Mocks.Schema(TestApp.DefaultId, @@ -226,7 +226,7 @@ public class GraphQLIntrospectionTests : GraphQLTestBase } [Fact] - public async Task Should_create_schema_with_duplicate_field_names() + public async Task Should_build_graphql_schema_on_duplicate_field_name() { var schema = Mocks.Schema(TestApp.DefaultId, @@ -244,7 +244,7 @@ public class GraphQLIntrospectionTests : GraphQLTestBase } [Fact] - public async Task Should_not_create_schema_With_invalid_component() + public async Task Should_not_build_grapqhl_schema_on_invalid_component() { var schema = Mocks.Schema(TestApp.DefaultId, @@ -262,7 +262,7 @@ public class GraphQLIntrospectionTests : GraphQLTestBase } [Fact] - public async Task Should_not_create_schema_With_invalid_components() + public async Task Should_not_build_grapqhl_schema_on_invalid_components() { var schema = Mocks.Schema(TestApp.DefaultId, @@ -280,7 +280,7 @@ public class GraphQLIntrospectionTests : GraphQLTestBase } [Fact] - public async Task Should_not_create_schema_With_invalid_references() + public async Task Should_not_build_grapqhl_schema_on_invalid_references() { var schema = Mocks.Schema(TestApp.DefaultId, diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs index 829f0e2e3..1e6377941 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs @@ -27,7 +27,7 @@ public class GraphQLMutationTests : GraphQLTestBase { content = TestContent.Create(contentId, TestSchemas.Ref1.Id, TestSchemas.Ref2.Id, null); - A.CallTo(() => commandBus.PublishAsync(A.Ignored, A._)) + A.CallTo(() => commandBus.PublishAsync(A.Ignored,A._)) .Returns(commandContext); } 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 fe915b5a7..a388cd3e3 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 @@ -47,6 +47,9 @@ public class GraphQLQueriesTests : GraphQLTestBase [Fact] public async Task Should_query_contents_with_full_text() { + var contentId = DomainId.NewGuid(); + var content = TestContent.Create(contentId); + var query = CreateQuery(@" query { queryMySchemaContents(search: ""Hello"") { @@ -54,11 +57,9 @@ public class GraphQLQueriesTests : GraphQLTestBase } }"); - 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._)) + 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 }); @@ -77,6 +78,93 @@ public class GraphQLQueriesTests : GraphQLTestBase AssertResult(expected, actual); } + [Fact] + public async Task Should_query_contents_with_ids() + { + var contentId = DomainId.NewGuid(); + var content = TestContent.Create(contentId); + + var query = CreateQuery(@" + query { + queryContents(ids: [""""]) { + ... on Content { + id + } + ... on MySchema { + flatData { + myNumber + } + } + } + }", contentId); + + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), + A.That.HasIds(contentId), + A._)) + .Returns(ResultList.CreateFrom(0, content)); + + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); + + var expected = new + { + data = new + { + queryContents = new[] + { + new + { + id = contentId, + flatData = new + { + myNumber = 1 + } + } + } + } + }; + + AssertResult(expected, actual); + } + + [Fact] + public async Task Should_query_contents_with_ids_and_dynamic_data() + { + var contentId = DomainId.NewGuid(); + var content = TestContent.Create(contentId); + + var query = CreateQuery(@" + query { + queryContents(ids: [""""]) { + ... on Content { + data: data__dynamic + } + } + }", contentId); + + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), + A.That.HasIds(contentId), + A._)) + .Returns(ResultList.CreateFrom(0, content)); + + var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); + + var expected = new + { + data = new + { + queryContents = new[] + { + new + { + data = content.Data + } + } + } + }; + + AssertResult(expected, actual); + } + [Fact] public async Task Should_return_multiple_assets_if_querying_assets() { @@ -90,7 +178,8 @@ public class GraphQLQueriesTests : GraphQLTestBase var asset = TestAsset.Create(DomainId.NewGuid()); A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, - A.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5&$filter=my-query" && x.NoTotal), A._)) + A.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5&$filter=my-query" && x.NoTotal), + A._)) .Returns(ResultList.CreateFrom(0, asset)); var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); @@ -125,7 +214,8 @@ public class GraphQLQueriesTests : GraphQLTestBase var asset = TestAsset.Create(DomainId.NewGuid()); A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, - A.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5&$filter=my-query" && !x.NoTotal), A._)) + A.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5&$filter=my-query" && !x.NoTotal), + A._)) .Returns(ResultList.CreateFrom(10, asset)); var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); @@ -161,7 +251,8 @@ public class GraphQLQueriesTests : GraphQLTestBase }", assetId); A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, - A.That.HasIdsWithoutTotal(assetId), A._)) + A.That.HasIdsWithoutTotal(assetId), + A._)) .Returns(ResultList.CreateFrom(1)); var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); @@ -191,7 +282,8 @@ public class GraphQLQueriesTests : GraphQLTestBase }", assetId); A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, - A.That.HasIdsWithoutTotal(assetId), A._)) + A.That.HasIdsWithoutTotal(assetId), + A._)) .Returns(ResultList.CreateFrom(1, asset)); var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); @@ -210,6 +302,9 @@ public class GraphQLQueriesTests : GraphQLTestBase [Fact] public async Task Should_return_multiple_flat_contents_if_querying_contents() { + var contentId = DomainId.NewGuid(); + var content = TestContent.Create(contentId); + var query = CreateQuery(@" query { queryMySchemaContents(top: 30, skip: 5) { @@ -217,11 +312,9 @@ public class GraphQLQueriesTests : GraphQLTestBase } }"); - var contentId = DomainId.NewGuid(); - var content = TestContent.Create(contentId); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), TestSchemas.Default.Id.ToString(), - A.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.NoTotal), A._)) + A.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.NoTotal), + A._)) .Returns(ResultList.CreateFrom(0, content)); var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); @@ -243,6 +336,9 @@ public class GraphQLQueriesTests : GraphQLTestBase [Fact] public async Task Should_return_multiple_contents_if_querying_contents() { + var contentId = DomainId.NewGuid(); + var content = TestContent.Create(contentId); + var query = CreateQuery(@" query { queryMySchemaContents(top: 30, skip: 5) { @@ -250,11 +346,9 @@ public class GraphQLQueriesTests : GraphQLTestBase } }"); - var contentId = DomainId.NewGuid(); - var content = TestContent.Create(contentId); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), TestSchemas.Default.Id.ToString(), - A.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.NoTotal), A._)) + A.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.NoTotal), + A._)) .Returns(ResultList.CreateFrom(0, content)); var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); @@ -276,6 +370,9 @@ public class GraphQLQueriesTests : GraphQLTestBase [Fact] public async Task Should_return_multiple_contents_with_total_if_querying_contents_with_total() { + var contentId = DomainId.NewGuid(); + var content = TestContent.Create(contentId); + var query = CreateQuery(@" query { queryMySchemaContentsWithTotal(top: 30, skip: 5) { @@ -286,11 +383,9 @@ public class GraphQLQueriesTests : GraphQLTestBase } }"); - var contentId = DomainId.NewGuid(); - var content = TestContent.Create(contentId); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), TestSchemas.Default.Id.ToString(), - A.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && !x.NoTotal), A._)) + A.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && !x.NoTotal), + A._)) .Returns(ResultList.CreateFrom(10, content)); var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); @@ -326,7 +421,8 @@ public class GraphQLQueriesTests : GraphQLTestBase }", contentId); A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), - A.That.HasIdsWithoutTotal(contentId), A._)) + A.That.HasIdsWithoutTotal(contentId), + A._)) .Returns(ResultList.CreateFrom(1)); var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); @@ -356,7 +452,8 @@ public class GraphQLQueriesTests : GraphQLTestBase }", contentId); A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), - A.That.HasIdsWithoutTotal(contentId), A._)) + A.That.HasIdsWithoutTotal(contentId), + A._)) .Returns(ResultList.CreateFrom(10, content)); var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); @@ -386,7 +483,8 @@ public class GraphQLQueriesTests : GraphQLTestBase }", contentId); A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), - A.That.HasIdsWithoutTotal(contentId), A._)) + A.That.HasIdsWithoutTotal(contentId), + A._)) .Returns(ResultList.CreateFrom(1, content)); var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); @@ -415,7 +513,8 @@ public class GraphQLQueriesTests : GraphQLTestBase } }", contentId); - A.CallTo(() => contentQuery.FindAsync(MatchsContentContext(), TestSchemas.Default.Id.ToString(), contentId, 3, A._)) + A.CallTo(() => contentQuery.FindAsync(MatchsContentContext(), TestSchemas.Default.Id.ToString(), contentId, 3, + A._)) .Returns(content); var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); @@ -467,11 +566,13 @@ public class GraphQLQueriesTests : GraphQLTestBase }", contentId); A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), - A.That.HasIdsWithoutTotal(contentRefId), A._)) + A.That.HasIdsWithoutTotal(contentRefId), + A._)) .Returns(ResultList.CreateFrom(0, contentRef)); A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), - A.That.HasIdsWithoutTotal(contentId), A._)) + A.That.HasIdsWithoutTotal(contentId), + A._)) .Returns(ResultList.CreateFrom(1, content)); var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); @@ -543,11 +644,13 @@ public class GraphQLQueriesTests : GraphQLTestBase }", contentId); A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), - A.That.HasIdsWithoutTotal(contentRefId), A._)) + A.That.HasIdsWithoutTotal(contentRefId), + A._)) .Returns(ResultList.CreateFrom(0, contentRef)); A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), - A.That.HasIdsWithoutTotal(contentId), A._)) + A.That.HasIdsWithoutTotal(contentId), + A._)) .Returns(ResultList.CreateFrom(1, content)); var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); @@ -608,11 +711,13 @@ public class GraphQLQueriesTests : GraphQLTestBase }", contentId); A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), - A.That.HasIdsWithoutTotal(contentRefId), A._)) + A.That.HasIdsWithoutTotal(contentRefId), + A._)) .Returns(ResultList.CreateFrom(0, contentRef)); A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), - A.That.HasIdsWithoutTotal(contentId), A._)) + A.That.HasIdsWithoutTotal(contentId), + A._)) .Returns(ResultList.CreateFrom(1, content)); var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); @@ -663,11 +768,13 @@ public class GraphQLQueriesTests : GraphQLTestBase }", contentId); A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), - A.That.HasIdsWithoutTotal(contentRefId), A._)) + A.That.HasIdsWithoutTotal(contentRefId), + A._)) .Returns(ResultList.CreateFrom(0, contentRef)); A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), - A.That.HasIdsWithoutTotal(contentId), A._)) + A.That.HasIdsWithoutTotal(contentId), + A._)) .Returns(ResultList.CreateFrom(1, content)); var actual1 = await ExecuteAsync(new ExecutionOptions { Query = query }); @@ -698,7 +805,8 @@ public class GraphQLQueriesTests : GraphQLTestBase AssertResult(expected, actual2); A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), - A.That.HasIdsWithoutTotal(contentRefId), A._)) + A.That.HasIdsWithoutTotal(contentRefId), + A._)) .MustHaveHappenedOnceExactly(); } @@ -731,7 +839,8 @@ public class GraphQLQueriesTests : GraphQLTestBase .Returns(ResultList.CreateFrom(1, contentRef)); A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), content.SchemaId.Id.ToString(), - A.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.Reference == contentRefId && x.NoTotal), A._)) + A.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.Reference == contentRefId && x.NoTotal), + A._)) .Returns(ResultList.CreateFrom(1, content)); var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); @@ -796,7 +905,8 @@ public class GraphQLQueriesTests : GraphQLTestBase .Returns(ResultList.CreateFrom(1, contentRef)); A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), content.SchemaId.Id.ToString(), - A.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.Reference == contentRefId && !x.NoTotal), A._)) + A.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.Reference == contentRefId && !x.NoTotal), + A._)) .Returns(ResultList.CreateFrom(10, content)); var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); @@ -857,7 +967,8 @@ public class GraphQLQueriesTests : GraphQLTestBase .Returns(ResultList.CreateFrom(1, content)); A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), contentRef.SchemaId.Id.ToString(), - A.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.Referencing == contentId && x.NoTotal), A._)) + A.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.Referencing == contentId && x.NoTotal), + A._)) .Returns(ResultList.CreateFrom(1, contentRef)); var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); @@ -910,7 +1021,8 @@ public class GraphQLQueriesTests : GraphQLTestBase .Returns(ResultList.CreateFrom(1, content)); A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), contentRef.SchemaId.Id.ToString(), - A.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.Referencing == contentId), A._)) + A.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.Referencing == contentId), + A._)) .Returns(ResultList.CreateFrom(10, contentRef)); var actual = await ExecuteAsync(new ExecutionOptions { Query = query }); diff --git a/tools/TestSuite/TestSuite.ApiTests/GraphQLFixture.cs b/tools/TestSuite/TestSuite.ApiTests/GraphQLFixture.cs index 8dde67ec3..7092590f2 100644 --- a/tools/TestSuite/TestSuite.ApiTests/GraphQLFixture.cs +++ b/tools/TestSuite/TestSuite.ApiTests/GraphQLFixture.cs @@ -14,6 +14,21 @@ namespace TestSuite.ApiTests; public sealed class GraphQLFixture : ContentFixture { + public IContentsClient Cities + { + get => Client.Contents("cities"); + } + + public IContentsClient Countries + { + get => Client.Contents("countries"); + } + + public IContentsClient States + { + get => Client.Contents("states"); + } + public sealed class DynamicEntity : Content { } @@ -121,17 +136,16 @@ public sealed class GraphQLFixture : ContentFixture private async Task CreateContentsAsync() { - var countriesClient = Client.Contents("countries"); - var countriesResponse = await countriesClient.GetAsync(); + var countries = await Countries.GetAsync(); - if (countriesResponse.Total > 0) + if (countries.Total > 0) { return; } async Task CreateCityAsync(string name) { - var citySAData = new + var cityData = new { name = new { @@ -139,15 +153,14 @@ public sealed class GraphQLFixture : ContentFixture } }; - var citiesClient = Client.Contents("cities"); - var cityResponse = await citiesClient.CreateAsync(citySAData, ContentCreateOptions.AsPublish); + var city = await Cities.CreateAsync(cityData, ContentCreateOptions.AsPublish); - return cityResponse.Id; + return city.Id; } async Task CreateStateAsync(string name, string cityId) { - var citySAData = new + var stateData = new { name = new { @@ -159,20 +172,19 @@ public sealed class GraphQLFixture : ContentFixture } }; - var statesClient = Client.Contents("states"); - var stateResponse = await statesClient.CreateAsync(citySAData, ContentCreateOptions.AsPublish); + var state = await States.CreateAsync(stateData, ContentCreateOptions.AsPublish); - return stateResponse.Id; + return state.Id; } - // STEP 1: Create state. 1 - var sachsenCapital = await CreateCityAsync("Leipzig"); - var sachstenState = await CreateStateAsync("Sachsen", sachsenCapital); + // STEP 1: Create state 1. + var saxonyCapital = await CreateCityAsync("Leipzig"); + var saxonyState = await CreateStateAsync("Saxony", saxonyCapital); - // STEP 1: Create state. 2 - var badenWCapital = await CreateCityAsync("Stuttgart"); - var badenWState = await CreateStateAsync("Baden Württemberg", badenWCapital); + // STEP 1: Create state 2. + var bavariaCapital = await CreateCityAsync("Munich"); + var bavariaState = await CreateStateAsync("Bavaria", bavariaCapital); // STEP 3: Create country. @@ -184,10 +196,10 @@ public sealed class GraphQLFixture : ContentFixture }, states = new { - iv = new[] { sachstenState, badenWState } + iv = new[] { saxonyState, bavariaState } } }; - await countriesClient.CreateAsync(countryData, ContentCreateOptions.AsPublish); + await Countries.CreateAsync(countryData, ContentCreateOptions.AsPublish); } } diff --git a/tools/TestSuite/TestSuite.ApiTests/GraphQLTests.cs b/tools/TestSuite/TestSuite.ApiTests/GraphQLTests.cs index d66abccfa..b57bec2e2 100644 --- a/tools/TestSuite/TestSuite.ApiTests/GraphQLTests.cs +++ b/tools/TestSuite/TestSuite.ApiTests/GraphQLTests.cs @@ -62,7 +62,7 @@ public sealed class GraphQLTests : IClassFixture } [Fact] - public async Task Should_query_graphql_reference_selectors() + public async Task Should_query_graphql_by_reference_selector() { var query = new { @@ -94,11 +94,11 @@ public sealed class GraphQLTests : IClassFixture .Select(x => x.Data.Name) .Order(); - Assert.Equal(new[] { "Leipzig", "Stuttgart" }, cityNames); + Assert.Equal(new[] { "Leipzig", "Munich" }, cityNames); } [Fact] - public async Task Should_query_graphql_reference_operator() + public async Task Should_query_graphql_by_references_function() { var query = new { @@ -131,11 +131,11 @@ public sealed class GraphQLTests : IClassFixture .Select(x => x["data"]["name"].Value()) .Order(); - Assert.Equal(new[] { "Leipzig", "Stuttgart" }, cityNames); + Assert.Equal(new[] { "Leipzig", "Munich" }, cityNames); } [Fact] - public async Task Should_query_graphql_reference_operator_with_filter() + public async Task Should_query_graphql_by_references_function_and_filter() { var query = new { @@ -172,7 +172,7 @@ public sealed class GraphQLTests : IClassFixture } [Fact] - public async Task Should_query_graphql_referencing_operator() + public async Task Should_query_graphql_by_referencing_function() { var query = new { @@ -196,18 +196,18 @@ public sealed class GraphQLTests : IClassFixture .Select(x => x["data"]["name"].Value()) .Order(); - Assert.Equal(new[] { "Baden Württemberg", "Sachsen" }, stateNames); + Assert.Equal(new[] { "Bavaria", "Saxony" }, stateNames); } [Fact] - public async Task Should_query_graphql_referencing_operator_with_filter() + public async Task Should_query_graphql_by_referencing_function_and_filter() { var query = new { query = @" { cities: queryCitiesContents { - states: referencingStatesContents(filter: ""data/name/iv eq 'Sachsen'"") { + states: referencingStatesContents(filter: ""data/name/iv eq 'Saxony'"") { data: flatData { name } @@ -224,7 +224,7 @@ public sealed class GraphQLTests : IClassFixture .Select(x => x["data"]["name"].Value()) .Order(); - Assert.Equal(new[] { "Sachsen" }, stateNames); + Assert.Equal(new[] { "Saxony" }, stateNames); } [Fact] @@ -248,6 +248,50 @@ public sealed class GraphQLTests : IClassFixture Assert.Equal("application/json", response.Content.Headers.ContentType.MediaType); } + [Fact] + public async Task Should_query_graphql_by_ids() + { + var allCities = await _.Cities.GetAsync(); + var allStates = await _.States.GetAsync(); + + var ids = allCities.Items.Select(x => x.Id).Union(allStates.Items.Select(x => x.Id)).ToList(); + + var query = new + { + query = @" + query ContentsQuery($ids: [String!]!) { + queryContents(ids: $ids) { + ... on Content { + id + } + ... on Cities { + data: flatData { + name + } + } + ... on States { + data: flatData { + name + } + } + } + }", + variables = new + { + ids + } + }; + + var result = await _.Client.SharedDynamicContents.GraphQlAsync(query); + + var names = + result["queryContents"] + .Select(x => x["data"]["name"].Value()) + .Order(); + + Assert.Equal(new[] { "Bavaria", "Leipzig", "Munich", "Saxony" }, names); + } + [Fact] public async Task Should_return_correct_vary_headers() {