Browse Source

Graphql query by ids (#985)

* Improve handling of default values.

* Open Api fixes and parent id.

* Query by IDs

* Introspection fixes.
pull/987/head
Sebastian Stehle 3 years ago
committed by GitHub
parent
commit
175711109d
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 9
      backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.Designer.cs
  2. 3
      backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.resx
  3. 4
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs
  4. 20
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ApplicationQueries.cs
  5. 32
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Builder.cs
  6. 20
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs
  7. 8
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentFields.cs
  8. 1
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentGraphType.cs
  9. 1
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentInterfaceGraphType.cs
  10. 6
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentUnionGraphType.cs
  11. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/EmbeddableStringGraphType.cs
  12. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldVisitor.cs
  13. 4
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs
  14. 24
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLIntrospectionTests.cs
  15. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs
  16. 186
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs
  17. 50
      tools/TestSuite/TestSuite.ApiTests/GraphQLFixture.cs
  18. 64
      tools/TestSuite/TestSuite.ApiTests/GraphQLTests.cs

9
backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.Designer.cs

@ -672,6 +672,15 @@ namespace Squidex.Domain.Apps.Core {
}
}
/// <summary>
/// Looks up a localized string similar to The list of IDs..
/// </summary>
public static string EntityIds {
get {
return ResourceManager.GetString("EntityIds", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to True when deleted..
/// </summary>

3
backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.resx

@ -321,6 +321,9 @@
<data name="EntityId" xml:space="preserve">
<value>The ID of the object (usually GUID).</value>
</data>
<data name="EntityIds" xml:space="preserve">
<value>The list of IDs.</value>
</data>
<data name="EntityIsDeleted" xml:space="preserve">
<value>True when deleted.</value>
</data>

4
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs

@ -154,7 +154,7 @@ public sealed class GraphQLExecutionContext : QueryExecutionContext
return dataLoaders.Context!.GetOrAddBatchLoader<DomainId, IEnrichedAssetEntity>(nameof(GetAssetsLoader),
async (batch, ct) =>
{
var result = await GetReferencedAssetsAsync(new List<DomainId>(batch), ct);
var result = await QueryAssetsByIdsAsync(new List<DomainId>(batch), ct);
return result.ToDictionary(x => x.Id);
});
@ -165,7 +165,7 @@ public sealed class GraphQLExecutionContext : QueryExecutionContext
return dataLoaders.Context!.GetOrAddBatchLoader<DomainId, IEnrichedContentEntity>(nameof(GetContentsLoader),
async (batch, ct) =>
{
var result = await GetReferencedContentsAsync(new List<DomainId>(batch), ct);
var result = await QueryContentsByIdsAsync(new List<DomainId>(batch), ct);
return result.ToDictionary(x => x.Id);
});

20
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."
});
}
}

32
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<SchemaInfo, ComponentGraphType> componentTypes = new Dictionary<SchemaInfo, ComponentGraphType>(ReferenceEqualityComparer.Instance);
private readonly Dictionary<SchemaInfo, ContentGraphType> contentTypes = new Dictionary<SchemaInfo, ContentGraphType>(ReferenceEqualityComparer.Instance);
private readonly Dictionary<SchemaInfo, ContentResultGraphType> contentResultTypes = new Dictionary<SchemaInfo, ContentResultGraphType>(ReferenceEqualityComparer.Instance);
private readonly Dictionary<FieldInfo, EmbeddableStringGraphType> embeddableStringTypes = new Dictionary<FieldInfo, EmbeddableStringGraphType>();
private readonly Dictionary<FieldInfo, ReferenceUnionGraphType> referenceUnionTypes = new Dictionary<FieldInfo, ReferenceUnionGraphType>();
private readonly Dictionary<FieldInfo, ComponentUnionGraphType> componentUnionTypes = new Dictionary<FieldInfo, ComponentUnionGraphType>();
private readonly Dictionary<FieldInfo, NestedGraphType> nestedTypes = new Dictionary<FieldInfo, NestedGraphType>();
private readonly Dictionary<string, EnumerationGraphType?> enumTypes = new Dictionary<string, EnumerationGraphType?>();
private readonly Dictionary<string, IGraphType[]> dynamicTypes = new Dictionary<string, IGraphType[]>();
private readonly Dictionary<FieldInfo, ComponentUnionGraphType> componentUnionTypes = new ();
private readonly Dictionary<FieldInfo, EmbeddableStringGraphType> embeddableStringTypes = new ();
private readonly Dictionary<FieldInfo, NestedGraphType> nestedTypes = new ();
private readonly Dictionary<SchemaInfo, ComponentGraphType> componentTypes = new ();
private readonly Dictionary<SchemaInfo, ContentGraphType> contentTypes = new ();
private readonly Dictionary<SchemaInfo, ContentResultGraphType> contentResultTypes = new ();
private readonly Dictionary<string, ContentUnionGraphType> unionTypes = new ();
private readonly Dictionary<string, EnumerationGraphType?> enumTypes = new ();
private readonly Dictionary<string, IGraphType[]> 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<string> values)
{
return enumTypes.GetOrAdd(name, x => FieldEnumType.TryCreate(name, values));
}
public ReferenceUnionGraphType GetReferenceUnion(FieldInfo fieldInfo, ReadonlyList<DomainId>? 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<DomainId>? 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<DomainId>? 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));

20
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<object, object?>(async (_, fieldContext, context) =>
{
var contentIds = fieldContext.GetArgument<DomainId[]>("ids");
return await context.QueryContentsByIdsAsync(contentIds, fieldContext.CancellationToken);
});
}
public static class QueryOrReferencing
{
public static readonly QueryArguments Arguments = new QueryArguments

8
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",

1
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentGraphType.cs

@ -42,6 +42,7 @@ internal sealed class ContentGraphType : ObjectGraphType<IEnrichedContentEntity>
AddField(ContentFields.StatusColor);
AddField(ContentFields.NewStatus);
AddField(ContentFields.NewStatusColor);
AddField(ContentFields.DataDynamic);
AddField(ContentFields.Url);
var contentDataType = new DataGraphType(builder, schemaInfo);

1
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentInterfaceGraphType.cs

@ -26,6 +26,7 @@ internal sealed class ContentInterfaceGraphType : InterfaceGraphType<IEnrichedCo
AddField(ContentFields.StatusColor);
AddField(ContentFields.NewStatus);
AddField(ContentFields.NewStatusColor);
AddField(ContentFields.DataDynamic);
AddField(ContentFields.Url);
Description = "The structure of all content types.";

6
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ReferenceUnionGraphType.cs → backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentUnionGraphType.cs

@ -11,16 +11,16 @@ using Squidex.Infrastructure.Collections;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents;
internal sealed class ReferenceUnionGraphType : UnionGraphType
internal sealed class ContentUnionGraphType : UnionGraphType
{
private readonly Dictionary<DomainId, IObjectGraphType> types = new Dictionary<DomainId, IObjectGraphType>();
public bool HasType => types.Count > 0;
public ReferenceUnionGraphType(Builder builder, FieldInfo fieldInfo, ReadonlyList<DomainId>? schemaIds)
public ContentUnionGraphType(Builder builder, string name, ReadonlyList<DomainId>? 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)
{

2
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/EmbeddableStringGraphType.cs

@ -48,7 +48,7 @@ internal sealed class EmbeddableStringGraphType : ObjectGraphType<string>
if (contentType == null)
{
var union = builder.GetReferenceUnion(fieldInfo, schemaIds);
var union = builder.GetContentUnion(fieldInfo.UnionReferenceType, schemaIds);
if (!union.HasType)
{

2
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldVisitor.cs

@ -263,7 +263,7 @@ internal sealed class FieldVisitor : IFieldVisitor<FieldGraphSchema, FieldInfo>
if (contentType == null)
{
var union = builder.GetReferenceUnion(fieldInfo, schemaIds);
var union = builder.GetContentUnion(fieldInfo.UnionReferenceType, schemaIds);
if (!union.HasType)
{

4
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs

@ -90,7 +90,7 @@ public abstract class QueryExecutionContext : Dictionary<string, object?>
return contents;
}
public virtual async Task<IReadOnlyList<IEnrichedAssetEntity>> GetReferencedAssetsAsync(IEnumerable<DomainId> ids,
public virtual async Task<IReadOnlyList<IEnrichedAssetEntity>> QueryAssetsByIdsAsync(IEnumerable<DomainId> ids,
CancellationToken ct)
{
Guard.NotNull(ids);
@ -111,7 +111,7 @@ public abstract class QueryExecutionContext : Dictionary<string, object?>
});
}
public virtual async Task<IReadOnlyList<IEnrichedContentEntity>> GetReferencedContentsAsync(IEnumerable<DomainId> ids,
public virtual async Task<IReadOnlyList<IEnrichedContentEntity>> QueryContentsByIdsAsync(IEnumerable<DomainId> ids,
CancellationToken ct)
{
Guard.NotNull(ids);

24
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,

2
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<ICommand>.Ignored, A<CancellationToken>._))
A.CallTo(() => commandBus.PublishAsync(A<ICommand>.Ignored,A<CancellationToken>._))
.Returns(commandContext);
}

186
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<Q>.That.Matches(x => x.QueryAsOdata == "?$skip=0&$search=\"Hello\"" && x.NoTotal), A<CancellationToken>._))
A<Q>.That.Matches(x => x.QueryAsOdata == "?$skip=0&$search=\"Hello\"" && x.NoTotal),
A<CancellationToken>._))
.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: [""<ID>""]) {
... on Content {
id
}
... on MySchema {
flatData {
myNumber
}
}
}
}", contentId);
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(),
A<Q>.That.HasIds(contentId),
A<CancellationToken>._))
.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: [""<ID>""]) {
... on Content {
data: data__dynamic
}
}
}", contentId);
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(),
A<Q>.That.HasIds(contentId),
A<CancellationToken>._))
.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<Q>.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5&$filter=my-query" && x.NoTotal), A<CancellationToken>._))
A<Q>.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5&$filter=my-query" && x.NoTotal),
A<CancellationToken>._))
.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<Q>.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5&$filter=my-query" && !x.NoTotal), A<CancellationToken>._))
A<Q>.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5&$filter=my-query" && !x.NoTotal),
A<CancellationToken>._))
.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<Q>.That.HasIdsWithoutTotal(assetId), A<CancellationToken>._))
A<Q>.That.HasIdsWithoutTotal(assetId),
A<CancellationToken>._))
.Returns(ResultList.CreateFrom<IEnrichedAssetEntity>(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<Q>.That.HasIdsWithoutTotal(assetId), A<CancellationToken>._))
A<Q>.That.HasIdsWithoutTotal(assetId),
A<CancellationToken>._))
.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<Q>.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.NoTotal), A<CancellationToken>._))
A<Q>.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.NoTotal),
A<CancellationToken>._))
.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<Q>.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.NoTotal), A<CancellationToken>._))
A<Q>.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.NoTotal),
A<CancellationToken>._))
.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<Q>.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && !x.NoTotal), A<CancellationToken>._))
A<Q>.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && !x.NoTotal),
A<CancellationToken>._))
.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<Q>.That.HasIdsWithoutTotal(contentId), A<CancellationToken>._))
A<Q>.That.HasIdsWithoutTotal(contentId),
A<CancellationToken>._))
.Returns(ResultList.CreateFrom<IEnrichedContentEntity>(1));
var actual = await ExecuteAsync(new ExecutionOptions { Query = query });
@ -356,7 +452,8 @@ public class GraphQLQueriesTests : GraphQLTestBase
}", contentId);
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(),
A<Q>.That.HasIdsWithoutTotal(contentId), A<CancellationToken>._))
A<Q>.That.HasIdsWithoutTotal(contentId),
A<CancellationToken>._))
.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<Q>.That.HasIdsWithoutTotal(contentId), A<CancellationToken>._))
A<Q>.That.HasIdsWithoutTotal(contentId),
A<CancellationToken>._))
.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<CancellationToken>._))
A.CallTo(() => contentQuery.FindAsync(MatchsContentContext(), TestSchemas.Default.Id.ToString(), contentId, 3,
A<CancellationToken>._))
.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<Q>.That.HasIdsWithoutTotal(contentRefId), A<CancellationToken>._))
A<Q>.That.HasIdsWithoutTotal(contentRefId),
A<CancellationToken>._))
.Returns(ResultList.CreateFrom(0, contentRef));
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(),
A<Q>.That.HasIdsWithoutTotal(contentId), A<CancellationToken>._))
A<Q>.That.HasIdsWithoutTotal(contentId),
A<CancellationToken>._))
.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<Q>.That.HasIdsWithoutTotal(contentRefId), A<CancellationToken>._))
A<Q>.That.HasIdsWithoutTotal(contentRefId),
A<CancellationToken>._))
.Returns(ResultList.CreateFrom(0, contentRef));
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(),
A<Q>.That.HasIdsWithoutTotal(contentId), A<CancellationToken>._))
A<Q>.That.HasIdsWithoutTotal(contentId),
A<CancellationToken>._))
.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<Q>.That.HasIdsWithoutTotal(contentRefId), A<CancellationToken>._))
A<Q>.That.HasIdsWithoutTotal(contentRefId),
A<CancellationToken>._))
.Returns(ResultList.CreateFrom(0, contentRef));
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(),
A<Q>.That.HasIdsWithoutTotal(contentId), A<CancellationToken>._))
A<Q>.That.HasIdsWithoutTotal(contentId),
A<CancellationToken>._))
.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<Q>.That.HasIdsWithoutTotal(contentRefId), A<CancellationToken>._))
A<Q>.That.HasIdsWithoutTotal(contentRefId),
A<CancellationToken>._))
.Returns(ResultList.CreateFrom(0, contentRef));
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(),
A<Q>.That.HasIdsWithoutTotal(contentId), A<CancellationToken>._))
A<Q>.That.HasIdsWithoutTotal(contentId),
A<CancellationToken>._))
.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<Q>.That.HasIdsWithoutTotal(contentRefId), A<CancellationToken>._))
A<Q>.That.HasIdsWithoutTotal(contentRefId),
A<CancellationToken>._))
.MustHaveHappenedOnceExactly();
}
@ -731,7 +839,8 @@ public class GraphQLQueriesTests : GraphQLTestBase
.Returns(ResultList.CreateFrom(1, contentRef));
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), content.SchemaId.Id.ToString(),
A<Q>.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.Reference == contentRefId && x.NoTotal), A<CancellationToken>._))
A<Q>.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.Reference == contentRefId && x.NoTotal),
A<CancellationToken>._))
.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<Q>.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.Reference == contentRefId && !x.NoTotal), A<CancellationToken>._))
A<Q>.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.Reference == contentRefId && !x.NoTotal),
A<CancellationToken>._))
.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<Q>.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.Referencing == contentId && x.NoTotal), A<CancellationToken>._))
A<Q>.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.Referencing == contentId && x.NoTotal),
A<CancellationToken>._))
.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<Q>.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.Referencing == contentId), A<CancellationToken>._))
A<Q>.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.Referencing == contentId),
A<CancellationToken>._))
.Returns(ResultList.CreateFrom(10, contentRef));
var actual = await ExecuteAsync(new ExecutionOptions { Query = query });

50
tools/TestSuite/TestSuite.ApiTests/GraphQLFixture.cs

@ -14,6 +14,21 @@ namespace TestSuite.ApiTests;
public sealed class GraphQLFixture : ContentFixture
{
public IContentsClient<DynamicEntity, object> Cities
{
get => Client.Contents<DynamicEntity, object>("cities");
}
public IContentsClient<DynamicEntity, object> Countries
{
get => Client.Contents<DynamicEntity, object>("countries");
}
public IContentsClient<DynamicEntity, object> States
{
get => Client.Contents<DynamicEntity, object>("states");
}
public sealed class DynamicEntity : Content<object>
{
}
@ -121,17 +136,16 @@ public sealed class GraphQLFixture : ContentFixture
private async Task CreateContentsAsync()
{
var countriesClient = Client.Contents<DynamicEntity, object>("countries");
var countriesResponse = await countriesClient.GetAsync();
var countries = await Countries.GetAsync();
if (countriesResponse.Total > 0)
if (countries.Total > 0)
{
return;
}
async Task<string> CreateCityAsync(string name)
{
var citySAData = new
var cityData = new
{
name = new
{
@ -139,15 +153,14 @@ public sealed class GraphQLFixture : ContentFixture
}
};
var citiesClient = Client.Contents<DynamicEntity, object>("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<string> 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<DynamicEntity, object>("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);
}
}

64
tools/TestSuite/TestSuite.ApiTests/GraphQLTests.cs

@ -62,7 +62,7 @@ public sealed class GraphQLTests : IClassFixture<GraphQLFixture>
}
[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<GraphQLFixture>
.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<GraphQLFixture>
.Select(x => x["data"]["name"].Value<string>())
.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<GraphQLFixture>
}
[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<GraphQLFixture>
.Select(x => x["data"]["name"].Value<string>())
.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<GraphQLFixture>
.Select(x => x["data"]["name"].Value<string>())
.Order();
Assert.Equal(new[] { "Sachsen" }, stateNames);
Assert.Equal(new[] { "Saxony" }, stateNames);
}
[Fact]
@ -248,6 +248,50 @@ public sealed class GraphQLTests : IClassFixture<GraphQLFixture>
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<JToken>(query);
var names =
result["queryContents"]
.Select(x => x["data"]["name"].Value<string>())
.Order();
Assert.Equal(new[] { "Bavaria", "Leipzig", "Munich", "Saxony" }, names);
}
[Fact]
public async Task Should_return_correct_vary_headers()
{

Loading…
Cancel
Save