diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs index e2915016a..8ac7242bf 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs @@ -90,7 +90,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL this.schemas = schemas.ToDictionary(x => x.Id); - graphQLSchema = new GraphQLSchema { Query = new ContentQueryGraphType(this, this.schemas.Values) }; + graphQLSchema = new GraphQLSchema { Query = new AppQueriesGraphType(this, this.schemas.Values) }; foreach (var schemaType in schemaTypes.Values) { diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs new file mode 100644 index 000000000..393dc904b --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs @@ -0,0 +1,246 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using GraphQL.Resolvers; +using GraphQL.Types; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +{ + public sealed class AppQueriesGraphType : ObjectGraphType + { + public AppQueriesGraphType(IGraphQLContext ctx, IEnumerable schemas) + { + var assetType = ctx.GetAssetType(); + + AddAssetFind(assetType); + AddAssetsQueries(assetType); + + foreach (var schema in schemas) + { + var schemaName = schema.SchemaDef.Properties.Label.WithFallback(schema.SchemaDef.Name); + var schemaType = ctx.GetSchemaType(schema.Id); + + AddContentFind(schema, schemaType, schemaName); + AddContentQueries(ctx, schema, schemaType, schemaName); + } + + Description = "The app queries."; + } + + private void AddAssetFind(IGraphType assetType) + { + AddField(new FieldType + { + Name = "findAsset", + Arguments = CreateAssetFindArguments(), + ResolvedType = assetType, + Resolver = new FuncFieldResolver(c => + { + var context = (GraphQLQueryContext)c.UserContext; + var contentId = Guid.Parse(c.GetArgument("id", Guid.Empty.ToString())); + + return context.FindAssetAsync(contentId); + }), + Description = "Find an asset by id." + }); + } + + private void AddContentFind(ISchemaEntity schema, IGraphType schemaType, string schemaName) + { + AddField(new FieldType + { + Name = $"find{schema.Name.ToPascalCase()}Content", + Arguments = CreateContentFindTypes(schemaName), + ResolvedType = schemaType, + Resolver = new FuncFieldResolver(c => + { + var context = (GraphQLQueryContext)c.UserContext; + var contentId = Guid.Parse(c.GetArgument("id", Guid.Empty.ToString())); + + return context.FindContentAsync(schema.Id, contentId); + }), + Description = $"Find an {schemaName} content by id." + }); + } + + private void AddAssetsQueries(IGraphType assetType) + { + AddField(new FieldType + { + Name = "queryAssets", + Arguments = CreateAssetQueryArguments(), + ResolvedType = new ListGraphType(new NonNullGraphType(assetType)), + Resolver = new FuncFieldResolver(c => + { + var context = (GraphQLQueryContext)c.UserContext; + + var argTop = c.GetArgument("top", 20); + var argSkip = c.GetArgument("skip", 0); + var argQuery = c.GetArgument("search", string.Empty); + + return context.QueryAssetsAsync(argQuery, argSkip, argTop); + }), + Description = "Query assets items." + }); + + AddField(new FieldType + { + Name = "queryAssetsWithTotal", + Arguments = CreateAssetQueryArguments(), + ResolvedType = new AssetResultGraphType(assetType), + Resolver = new FuncFieldResolver(c => + { + var context = (GraphQLQueryContext)c.UserContext; + + var argTop = c.GetArgument("top", 20); + var argSkip = c.GetArgument("skip", 0); + var argQuery = c.GetArgument("search", string.Empty); + + return context.QueryAssetsAsync(argQuery, argSkip, argTop); + }), + Description = "Query assets items with total count." + }); + } + + private void AddContentQueries(IGraphQLContext ctx, ISchemaEntity schema, IGraphType schemaType, string schemaName) + { + AddField(new FieldType + { + Name = $"query{schema.Name.ToPascalCase()}Contents", + Arguments = CreateContentQueryArguments(), + ResolvedType = new ListGraphType(new NonNullGraphType(schemaType)), + Resolver = new FuncFieldResolver(c => + { + var context = (GraphQLQueryContext)c.UserContext; + var contentQuery = BuildODataQuery(c); + + return context.QueryContentsAsync(schema.Id.ToString(), contentQuery); + }), + Description = $"Query {schemaName} content items." + }); + + AddField(new FieldType + { + Name = $"query{schema.Name.ToPascalCase()}ContentsWithTotal", + Arguments = CreateContentQueryArguments(), + ResolvedType = new ContentResultGraphType(ctx, schema, schemaName), + Resolver = new FuncFieldResolver(c => + { + var context = (GraphQLQueryContext)c.UserContext; + var contentQuery = BuildODataQuery(c); + + return context.QueryContentsAsync(schema.Id.ToString(), contentQuery); + }), + Description = $"Query {schemaName} content items with total count." + }); + } + + private static QueryArguments CreateAssetFindArguments() + { + return new QueryArguments + { + new QueryArgument(typeof(StringGraphType)) + { + Name = "id", + Description = "The id of the asset.", + DefaultValue = string.Empty + } + }; + } + + private static QueryArguments CreateContentFindTypes(string schemaName) + { + return new QueryArguments + { + new QueryArgument(typeof(StringGraphType)) + { + Name = "id", + Description = $"The id of the {schemaName} content.", + DefaultValue = string.Empty + } + }; + } + + private static QueryArguments CreateAssetQueryArguments() + { + return new QueryArguments + { + new QueryArgument(typeof(IntGraphType)) + { + Name = "top", + Description = "Optional number of assets to take.", + DefaultValue = 20 + }, + new QueryArgument(typeof(IntGraphType)) + { + Name = "skip", + Description = "Optional number of assets to skip.", + DefaultValue = 0 + }, + new QueryArgument(typeof(StringGraphType)) + { + Name = "search", + Description = "Optional query.", + DefaultValue = string.Empty + } + }; + } + + private static QueryArguments CreateContentQueryArguments() + { + return new QueryArguments + { + new QueryArgument(typeof(IntGraphType)) + { + Name = "top", + Description = "Optional number of contents to take.", + DefaultValue = 20 + }, + new QueryArgument(typeof(IntGraphType)) + { + Name = "skip", + Description = "Optional number of contents to skip.", + DefaultValue = 0 + }, + new QueryArgument(typeof(StringGraphType)) + { + Name = "filter", + Description = "Optional OData filter.", + DefaultValue = string.Empty + }, + new QueryArgument(typeof(StringGraphType)) + { + Name = "search", + Description = "Optional OData full text search.", + DefaultValue = string.Empty + }, + new QueryArgument(typeof(StringGraphType)) + { + Name = "orderby", + Description = "Optional OData order definition.", + DefaultValue = string.Empty + } + }; + } + + private static string BuildODataQuery(ResolveFieldContext c) + { + var odataQuery = "?" + + string.Join("&", + c.Arguments + .Select(x => new { x.Key, Value = x.Value.ToString() }).Where(x => !string.IsNullOrWhiteSpace(x.Value)) + .Select(x => $"${x.Key}={x.Value}")); + + return odataQuery; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetResultGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetResultGraphType.cs new file mode 100644 index 000000000..d38478a33 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetResultGraphType.cs @@ -0,0 +1,44 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using GraphQL.Resolvers; +using GraphQL.Types; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +{ + public sealed class AssetResultGraphType : ObjectGraphType> + { + public AssetResultGraphType(IGraphType assetType) + { + Name = $"AssetResultDto"; + + AddField(new FieldType + { + Name = "total", + Resolver = Resolver(x => x.Total), + ResolvedType = new NonNullGraphType(new IntGraphType()), + Description = $"The total number of asset." + }); + + AddField(new FieldType + { + Name = "items", + Resolver = Resolver(x => x), + ResolvedType = new ListGraphType(new NonNullGraphType(assetType)), + Description = $"The assets." + }); + } + + private static IFieldResolver Resolver(Func, object> action) + { + return new FuncFieldResolver, object>(c => action(c.Source)); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs index def6b2251..f1eb1faba 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs @@ -16,7 +16,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { public sealed class ContentDataGraphType : ObjectGraphType { - public ContentDataGraphType(Schema schema, IGraphQLContext context) + public ContentDataGraphType(Schema schema, IGraphQLContext qlContext) { var schemaName = schema.Properties.Label.WithFallback(schema.Name); @@ -24,7 +24,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types foreach (var field in schema.Fields.Where(x => !x.IsHidden)) { - var fieldInfo = context.GetGraphType(field); + var fieldInfo = qlContext.GetGraphType(field); if (fieldInfo.ResolveType != null) { @@ -35,7 +35,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types Name = $"{schema.Name.ToPascalCase()}Data{field.Name.ToPascalCase()}Dto" }; - var partition = context.ResolvePartition(field.Partitioning); + var partition = qlContext.ResolvePartition(field.Partitioning); foreach (var partitionItem in partition) { diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs index 1d0348db9..f17cfa1a0 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs @@ -17,11 +17,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types public sealed class ContentGraphType : ObjectGraphType { private readonly ISchemaEntity schema; - private readonly IGraphQLContext context; + private readonly IGraphQLContext ctx; - public ContentGraphType(ISchemaEntity schema, IGraphQLContext context) + public ContentGraphType(ISchemaEntity schema, IGraphQLContext ctx) { - this.context = context; + this.ctx = ctx; this.schema = schema; Name = $"{schema.Name.ToPascalCase()}Dto"; @@ -82,12 +82,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types AddField(new FieldType { Name = "url", - Resolver = context.ResolveContentUrl(schema), + Resolver = ctx.ResolveContentUrl(schema), ResolvedType = new NonNullGraphType(new StringGraphType()), Description = $"The url to the the {schemaName} content." }); - var dataType = new ContentDataGraphType(schema.SchemaDef, context); + var dataType = new ContentDataGraphType(schema.SchemaDef, ctx); if (dataType.Fields.Any()) { diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentQueryGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentQueryGraphType.cs deleted file mode 100644 index d6604ed50..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentQueryGraphType.cs +++ /dev/null @@ -1,191 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using GraphQL.Resolvers; -using GraphQL.Types; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types -{ - public sealed class ContentQueryGraphType : ObjectGraphType - { - public ContentQueryGraphType(IGraphQLContext graphQLContext, IEnumerable schemas) - { - AddAssetFind(graphQLContext); - AddAssetsQuery(graphQLContext); - - foreach (var schema in schemas) - { - var schemaName = schema.SchemaDef.Properties.Label.WithFallback(schema.SchemaDef.Name); - var schemaType = graphQLContext.GetSchemaType(schema.Id); - - AddContentFind(schema, schemaType, schemaName); - AddContentQuery(schema, schemaType, schemaName); - } - - Description = "The app queries."; - } - - private void AddAssetFind(IGraphQLContext graphQLContext) - { - AddField(new FieldType - { - Name = "findAsset", - Arguments = new QueryArguments - { - new QueryArgument(typeof(StringGraphType)) - { - Name = "id", - Description = "The id of the asset.", - DefaultValue = string.Empty - } - }, - ResolvedType = graphQLContext.GetAssetType(), - Resolver = new FuncFieldResolver(c => - { - var context = (GraphQLQueryContext)c.UserContext; - var contentId = Guid.Parse(c.GetArgument("id", Guid.Empty.ToString())); - - return context.FindAssetAsync(contentId); - }), - Description = "Find an asset by id." - }); - } - - private void AddContentFind(ISchemaEntity schema, IGraphType schemaType, string schemaName) - { - AddField(new FieldType - { - Name = $"find{schema.Name.ToPascalCase()}Content", - Arguments = new QueryArguments - { - new QueryArgument(typeof(StringGraphType)) - { - Name = "id", - Description = $"The id of the {schemaName} content.", - DefaultValue = string.Empty - } - }, - ResolvedType = schemaType, - Resolver = new FuncFieldResolver(c => - { - var context = (GraphQLQueryContext)c.UserContext; - var contentId = Guid.Parse(c.GetArgument("id", Guid.Empty.ToString())); - - return context.FindContentAsync(schema.Id, contentId); - }), - Description = $"Find an {schemaName} content by id." - }); - } - - private void AddAssetsQuery(IGraphQLContext graphQLContext) - { - AddField(new FieldType - { - Name = "queryAssets", - Arguments = new QueryArguments - { - new QueryArgument(typeof(IntGraphType)) - { - Name = "top", - Description = "Optional number of assets to take.", - DefaultValue = 20 - }, - new QueryArgument(typeof(IntGraphType)) - { - Name = "skip", - Description = "Optional number of assets to skip.", - DefaultValue = 0 - }, - new QueryArgument(typeof(StringGraphType)) - { - Name = "search", - Description = "Optional query.", - DefaultValue = string.Empty - } - }, - ResolvedType = new ListGraphType(new NonNullGraphType(graphQLContext.GetAssetType())), - Resolver = new FuncFieldResolver(c => - { - var context = (GraphQLQueryContext)c.UserContext; - - var argTop = c.GetArgument("top", 20); - var argSkip = c.GetArgument("skip", 0); - var argQuery = c.GetArgument("search", string.Empty); - - return context.QueryAssetsAsync(argQuery, argSkip, argTop); - }), - Description = "Query assets items." - }); - } - - private void AddContentQuery(ISchemaEntity schema, IGraphType schemaType, string schemaName) - { - AddField(new FieldType - { - Name = $"query{schema.Name.ToPascalCase()}Contents", - Arguments = new QueryArguments - { - new QueryArgument(typeof(IntGraphType)) - { - Name = "top", - Description = "Optional number of contents to take.", - DefaultValue = 20 - }, - new QueryArgument(typeof(IntGraphType)) - { - Name = "skip", - Description = "Optional number of contents to skip.", - DefaultValue = 0 - }, - new QueryArgument(typeof(StringGraphType)) - { - Name = "filter", - Description = "Optional OData filter.", - DefaultValue = string.Empty - }, - new QueryArgument(typeof(StringGraphType)) - { - Name = "search", - Description = "Optional OData full text search.", - DefaultValue = string.Empty - }, - new QueryArgument(typeof(StringGraphType)) - { - Name = "orderby", - Description = "Optional OData order definition.", - DefaultValue = string.Empty - } - }, - ResolvedType = new ListGraphType(new NonNullGraphType(schemaType)), - Resolver = new FuncFieldResolver(c => - { - var context = (GraphQLQueryContext)c.UserContext; - var contentQuery = BuildODataQuery(c); - - return context.QueryContentsAsync(schema.Id.ToString(), contentQuery); - }), - Description = $"Query {schemaName} content items." - }); - } - - private static string BuildODataQuery(ResolveFieldContext c) - { - var odataQuery = "?" + - string.Join("&", - c.Arguments - .Select(x => new { x.Key, Value = x.Value.ToString() }).Where(x => !string.IsNullOrWhiteSpace(x.Value)) - .Select(x => $"${x.Key}={x.Value}")); - - return odataQuery; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentResultGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentResultGraphType.cs new file mode 100644 index 000000000..1b6ebee7f --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentResultGraphType.cs @@ -0,0 +1,46 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using GraphQL.Resolvers; +using GraphQL.Types; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +{ + public sealed class ContentResultGraphType : ObjectGraphType> + { + public ContentResultGraphType(IGraphQLContext ctx, ISchemaEntity schema, string schemaName) + { + Name = $"{schema.Name.ToPascalCase()}ResultDto"; + + var schemaType = ctx.GetSchemaType(schema.Id); + + AddField(new FieldType + { + Name = "total", + Resolver = Resolver(x => x.Total), + ResolvedType = new NonNullGraphType(new IntGraphType()), + Description = $"The total number of {schemaName} items." + }); + + AddField(new FieldType + { + Name = "items", + Resolver = Resolver(x => x), + ResolvedType = new ListGraphType(new NonNullGraphType(schemaType)), + Description = $"The {schemaName} items." + }); + } + + private static IFieldResolver Resolver(Func, object> action) + { + return new FuncFieldResolver, object>(c => action(c.Source)); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/QueryContext.cs b/src/Squidex.Domain.Apps.Entities/Contents/QueryContext.cs index cf306c0ea..8ed70d159 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/QueryContext.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/QueryContext.cs @@ -80,7 +80,7 @@ namespace Squidex.Domain.Apps.Entities.Contents return content; } - public async Task> QueryAssetsAsync(string query, int skip = 0, int take = 10) + public async Task> QueryAssetsAsync(string query, int skip = 0, int take = 10) { var assets = await assetRepository.QueryAsync(app.Id, null, null, query, take, skip); @@ -92,7 +92,7 @@ namespace Squidex.Domain.Apps.Entities.Contents return assets; } - public async Task> QueryContentsAsync(string schemaIdOrName, string query) + public async Task> QueryContentsAsync(string schemaIdOrName, string query) { var result = await contentQuery.QueryAsync(app, schemaIdOrName, user, false, query); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTests.cs index 9a4fcab0d..d5f7e6176 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTests.cs @@ -172,6 +172,79 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL AssertJson(expected, new { data = result.Data }); } + [Fact] + public async Task Should_return_multiple_assets_with_total_when_querying_assets_with_total() + { + const string query = @" + query { + queryAssetsWithTotal(search: ""my-query"", top: 30, skip: 5) { + total + items { + id + version + created + createdBy + lastModified + lastModifiedBy + url + thumbnailUrl + sourceUrl + mimeType + fileName + fileSize + fileVersion + isImage + pixelWidth + pixelHeight + } + } + }"; + + var asset = CreateAsset(Guid.NewGuid()); + + var assets = new List { asset }; + + A.CallTo(() => assetRepository.QueryAsync(app.Id, null, null, "my-query", 30, 5)) + .Returns(ResultList.Create(assets, 10)); + + var result = await sut.QueryAsync(app, user, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + queryAssetsWithTotal = new + { + total = 10, + items = new dynamic[] + { + new + { + id = asset.Id, + version = 1, + created = asset.Created.ToDateTimeUtc(), + createdBy = "subject:user1", + lastModified = asset.LastModified.ToDateTimeUtc(), + lastModifiedBy = "subject:user2", + url = $"assets/{asset.Id}", + thumbnailUrl = $"assets/{asset.Id}?width=100", + sourceUrl = $"assets/source/{asset.Id}", + mimeType = "image/png", + fileName = "MyFile.png", + fileSize = 1024, + fileVersion = 123, + isImage = true, + pixelWidth = 800, + pixelHeight = 600 + } + } + } + } + }; + + AssertJson(expected, new { data = result.Data }); + } + [Fact] public async Task Should_return_single_asset_when_finding_asset() { @@ -347,6 +420,126 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL AssertJson(expected, new { data = result.Data }); } + [Fact] + public async Task Should_return_multiple_contents_with_total_when_querying_contents_with_total() + { + const string query = @" + query { + queryMySchemaContentsWithTotal(top: 30, skip: 5) { + total + items { + id + version + created + createdBy + lastModified + lastModifiedBy + url + data { + myString { + de + } + myNumber { + iv + } + myBoolean { + iv + } + myDatetime { + iv + } + myJson { + iv + } + myGeolocation { + iv + } + myTags { + iv + } + } + } + } + }"; + + var content = CreateContent(Guid.NewGuid(), Guid.Empty, Guid.Empty); + + var contents = new List { content }; + + A.CallTo(() => contentQuery.QueryAsync(app, schema.Id.ToString(), user, false, "?$top=30&$skip=5")) + .Returns((schema, ResultList.Create(contents, 10))); + + var result = await sut.QueryAsync(app, user, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + queryMySchemaContentsWithTotal = new + { + total = 10, + items = new dynamic[] + { + new + { + id = content.Id, + version = 1, + created = content.Created.ToDateTimeUtc(), + createdBy = "subject:user1", + lastModified = content.LastModified.ToDateTimeUtc(), + lastModifiedBy = "subject:user2", + url = $"contents/my-schema/{content.Id}", + data = new + { + myString = new + { + de = "value" + }, + myNumber = new + { + iv = 1 + }, + myBoolean = new + { + iv = true + }, + myDatetime = new + { + iv = content.LastModified.ToDateTimeUtc() + }, + myJson = new + { + iv = new + { + value = 1 + } + }, + myGeolocation = new + { + iv = new + { + latitude = 10, + longitude = 20 + } + }, + myTags = new + { + iv = new[] + { + "tag1", + "tag2" + } + } + } + } + } + } + } + }; + + AssertJson(expected, new { data = result.Data }); + } + [Fact] public async Task Should_return_single_content_when_finding_content() {