Browse Source

New graphql operations to also get total items.

pull/221/head
Sebastian Stehle 8 years ago
parent
commit
6e32fca7e7
  1. 2
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs
  2. 246
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs
  3. 44
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetResultGraphType.cs
  4. 6
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs
  5. 10
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs
  6. 191
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentQueryGraphType.cs
  7. 46
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentResultGraphType.cs
  8. 4
      src/Squidex.Domain.Apps.Entities/Contents/QueryContext.cs
  9. 193
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTests.cs

2
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)
{

246
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<ISchemaEntity> 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<object>(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<object>(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<object>(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<object>(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<object>(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<object>(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;
}
}
}

44
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<IResultList<IAssetEntity>>
{
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<IResultList<IAssetEntity>, object> action)
{
return new FuncFieldResolver<IResultList<IAssetEntity>, object>(c => action(c.Source));
}
}
}

6
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<NamedContentData>
{
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)
{

10
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<IContentEntity>
{
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())
{

191
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentQueryGraphType.cs

@ -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<ISchemaEntity> 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<object>(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<object>(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<object>(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<object>(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;
}
}
}

46
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<IResultList<IContentEntity>>
{
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<IResultList<IContentEntity>, object> action)
{
return new FuncFieldResolver<IResultList<IContentEntity>, object>(c => action(c.Source));
}
}
}

4
src/Squidex.Domain.Apps.Entities/Contents/QueryContext.cs

@ -80,7 +80,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
return content;
}
public async Task<IReadOnlyList<IAssetEntity>> QueryAssetsAsync(string query, int skip = 0, int take = 10)
public async Task<IResultList<IAssetEntity>> 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<IReadOnlyList<IContentEntity>> QueryContentsAsync(string schemaIdOrName, string query)
public async Task<IResultList<IContentEntity>> QueryContentsAsync(string schemaIdOrName, string query)
{
var result = await contentQuery.QueryAsync(app, schemaIdOrName, user, false, query);

193
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<IAssetEntity> { 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<IContentEntity> { 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()
{

Loading…
Cancel
Save