Skip to content

Commit

Permalink
[GraphQL] collapsed where statements fixes #3234 (#3235)
Browse files Browse the repository at this point in the history
  • Loading branch information
carlwoodhouse authored and sebastienros committed Feb 27, 2019
1 parent 79552ae commit 1e4e28d
Show file tree
Hide file tree
Showing 5 changed files with 183 additions and 37 deletions.
@@ -0,0 +1,38 @@
using System.Collections.Generic;
using GraphQL.Types;

namespace OrchardCore.ContentManagement.GraphQL
{
public static class FieldTypeExtensions
{
public static FieldType WithPartCollapsedMetaData(this FieldType fieldType, bool collapsed = true)
{
return fieldType.WithMetaData("PartCollapsed", collapsed);
}

public static FieldType WithPartNameMetaData(this FieldType fieldType, string partName)
{
return fieldType.WithMetaData("PartName", partName);
}

internal static FieldType WithMetaData(this FieldType fieldType, string name, object value)
{
if (fieldType == null)
{
return null;
}

if (fieldType.Metadata == null)
{
fieldType.Metadata = new Dictionary<string, object>();
}

if (!fieldType.HasMetadata(name))
{
fieldType.Metadata.Add(name, value);
}

return fieldType;
}
}
}
Expand Up @@ -8,6 +8,7 @@
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json.Linq;
using OrchardCore.Apis.GraphQL;
using OrchardCore.Apis.GraphQL.Queries;
using OrchardCore.ContentManagement.GraphQL.Queries.Predicates;
using OrchardCore.ContentManagement.GraphQL.Queries.Types;
using OrchardCore.ContentManagement.Records;
Expand Down Expand Up @@ -83,16 +84,17 @@ private async Task<IEnumerable<ContentItem>> Resolve(ResolveFieldContext context
query = FilterContentType(query, context);
query = OrderBy(query, context);

var contentItemsQuery = await FilterWhereArguments(query, where, session, graphContext);
var contentItemsQuery = await FilterWhereArguments(query, where, context, session, graphContext);
contentItemsQuery = PageQuery(contentItemsQuery, context);

return await contentItemsQuery.ListAsync();
}

private async Task<IQuery<ContentItem>> FilterWhereArguments(
IQuery<ContentItem, ContentItemIndex> query,
JObject where,
ISession session,
IQuery<ContentItem, ContentItemIndex> query,
JObject where,
ResolveFieldContext fieldContext,
ISession session,
GraphQLContext context)
{
if (where == null)
Expand All @@ -109,14 +111,14 @@ private async Task<IEnumerable<ContentItem>> Resolve(ResolveFieldContext context
// Add all provided table alias to the current predicate query
var providers = context.ServiceProvider.GetServices<IIndexAliasProvider>();
var indexes = new Dictionary<string, IndexAlias>(StringComparer.OrdinalIgnoreCase);
var indexAliases = new List<string>();
var indexAliases = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

foreach (var aliasProvider in providers)
{
foreach (var alias in aliasProvider.GetAliases())
{
predicateQuery.CreateAlias(alias.Alias, alias.Index);
indexAliases.Add(alias.Alias);
indexAliases.Add(alias.Alias, alias.Alias);

if (!indexes.ContainsKey(alias.Index))
{
Expand All @@ -126,7 +128,7 @@ private async Task<IEnumerable<ContentItem>> Resolve(ResolveFieldContext context
}

var expressions = Expression.Conjunction();
BuildWhereExpressions(where, expressions, null, indexAliases);
BuildWhereExpressions(where, expressions, null, fieldContext, indexAliases);

var whereSqlClause = expressions.ToSqlString(predicateQuery);
query = query.Where(whereSqlClause);
Expand Down Expand Up @@ -206,25 +208,25 @@ private VersionOptions GetVersionOption(PublicationStatusEnum status)
return query;
}

private void BuildWhereExpressions(JToken where, Junction expressions, string tableAlias, IEnumerable<string> indexAliases)
private void BuildWhereExpressions(JToken where, Junction expressions, string tableAlias, ResolveFieldContext fieldContext, IDictionary<string, string> indexAliases)
{
if (where is JArray array)
{
foreach (var child in array.Children())
{
if (child is JObject whereObject)
{
BuildExpressionsInternal(whereObject, expressions, tableAlias, indexAliases);
BuildExpressionsInternal(whereObject, expressions, tableAlias, fieldContext, indexAliases);
}
}
}
else if (where is JObject whereObject)
{
BuildExpressionsInternal(whereObject, expressions, tableAlias, indexAliases);
BuildExpressionsInternal(whereObject, expressions, tableAlias, fieldContext, indexAliases);
}
}

private void BuildExpressionsInternal(JObject where, Junction expressions, string tableAlias, IEnumerable<string> indexAliases)
private void BuildExpressionsInternal(JObject where, Junction expressions, string tableAlias, ResolveFieldContext fieldContext, IDictionary<string, string> indexAliases)
{
foreach (var entry in where.Properties())
{
Expand All @@ -234,18 +236,31 @@ private void BuildExpressionsInternal(JObject where, Junction expressions, strin

// Gets the full path name without the comparison e.g. aliasPart.alias, not aliasPart.alias_contains.
var property = values[0];
if (!string.IsNullOrEmpty(tableAlias))

// figure out table aliases for collapsed parts and ones with the part suffix removed by the dsl
if (tableAlias == null || !tableAlias.EndsWith("Part", StringComparison.OrdinalIgnoreCase))
{
// check if we need to stick a suffix of 'Part' on the alias.
if (!tableAlias.EndsWith("Part")) {

var aliasLookup = indexAliases.FirstOrDefault(x => x.Equals($"{tableAlias}Part", StringComparison.OrdinalIgnoreCase));
if (aliasLookup != null)
var whereArgument = fieldContext?.FieldDefinition?.Arguments.FirstOrDefault(x => x.Name == "where");

if (whereArgument != null)
{
var whereInput = (WhereInputObjectGraphType)whereArgument.ResolvedType;

foreach (var field in whereInput.Fields.Where(x => x.GetMetadata<string>("PartName") != null))
{
tableAlias = aliasLookup;
var partName = field.GetMetadata<string>("PartName");
if ((tableAlias == null && field.GetMetadata<bool>("PartCollapsed") && field.Name.Equals(property, StringComparison.OrdinalIgnoreCase)) ||
(tableAlias != null && partName.ToFieldName().Equals(tableAlias, StringComparison.OrdinalIgnoreCase)))
{
tableAlias = indexAliases.TryGetValue(partName, out var indexTableAlias) ? indexTableAlias : tableAlias;
break;
}
}
}
}

if (tableAlias != null)
{
property = $"{tableAlias}.{property}";
}

Expand All @@ -254,24 +269,24 @@ private void BuildExpressionsInternal(JObject where, Junction expressions, strin
if (string.Equals(values[0], "or", StringComparison.OrdinalIgnoreCase))
{
expression = Expression.Disjunction();
BuildWhereExpressions(entry.Value, (Junction)expression, tableAlias, indexAliases);
BuildWhereExpressions(entry.Value, (Junction)expression, tableAlias, fieldContext, indexAliases);
}
else if (string.Equals(values[0], "and", StringComparison.OrdinalIgnoreCase))
{
expression = Expression.Conjunction();
BuildWhereExpressions(entry.Value, (Junction)expression, tableAlias, indexAliases);
BuildWhereExpressions(entry.Value, (Junction)expression, tableAlias, fieldContext, indexAliases);
}
else if (string.Equals(values[0], "not", StringComparison.OrdinalIgnoreCase))
{
expression = Expression.Conjunction();
BuildWhereExpressions(entry.Value, (Junction)expression, tableAlias, indexAliases);
BuildWhereExpressions(entry.Value, (Junction)expression, tableAlias, fieldContext, indexAliases);
expression = Expression.Not(expression);
}
else if (entry.HasValues && entry.Value.Type == JTokenType.Object)
{
// Loop through the part's properties, passing the name of the part as the table tableAlias.
// This tableAlias can then be used with the table alias to index mappings to join with the correct table.
BuildWhereExpressions(entry.Value, expressions, values[0], indexAliases);
BuildWhereExpressions(entry.Value, expressions, values[0], fieldContext, indexAliases);
}
else
{
Expand Down
@@ -1,7 +1,4 @@
using System.Collections.Generic;
using System.Linq;
using GraphQL;
using GraphQL.Execution;
using GraphQL.Resolvers;
using GraphQL.Types;
using Microsoft.AspNetCore.Http;
Expand Down Expand Up @@ -42,9 +39,11 @@ public void Build(FieldType contentQuery, ContentTypeDefinition contentTypeDefin

var queryGraphType = typeof(ObjectGraphType<>).MakeGenericType(activator.Type);

var collapsePart = _contentOptions.ShouldCollapse(part);

if (serviceProvider.GetService(queryGraphType) is IObjectGraphType queryGraphTypeResolved)
{
if (_contentOptions.ShouldCollapse(part))
if (collapsePart)
{
foreach (var field in queryGraphTypeResolved.Fields)
{
Expand All @@ -57,7 +56,8 @@ public void Build(FieldType contentQuery, ContentTypeDefinition contentTypeDefin
Description = field.Description,
DeprecationReason = field.DeprecationReason,
Arguments = field.Arguments,
Resolver = new FuncFieldResolver<ContentItem, object>(context => {
Resolver = new FuncFieldResolver<ContentItem, object>(context =>
{
var nameToResolve = partName;
var resolvedPart = context.Source.Get(activator.Type, nameToResolve);
Expand Down Expand Up @@ -102,13 +102,22 @@ public void Build(FieldType contentQuery, ContentTypeDefinition contentTypeDefin

var whereInput = (ContentItemWhereInput)whereArgument.ResolvedType;

whereInput.AddField(new FieldType
if (collapsePart)
{
foreach (var field in inputGraphTypeResolved.Fields)
{
whereInput.AddField(field.WithPartCollapsedMetaData().WithPartNameMetaData(partName));
}
}
else
{
Type = inputGraphTypeResolved.GetType(),
Name = partName.ToFieldName(),

Description = inputGraphTypeResolved.Description
});
whereInput.AddField(new FieldType
{
Type = inputGraphTypeResolved.GetType(),
Name = partName.ToFieldName(),
Description = inputGraphTypeResolved.Description
}.WithPartNameMetaData(partName));
}
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion test/OrchardCore.Tests/Apis/GraphQL/Blog/BlogPostTests.cs
Expand Up @@ -84,7 +84,7 @@ public async Task ShouldQueryByBlogPostAutoroutePart()
.Query("BlogPost", builder =>
{
builder
.WithNestedQueryField("Autoroute", "path: \"Path1\"");
.WithQueryField("path", "Path1");
builder
.AddField("DisplayText");
Expand Down
90 changes: 87 additions & 3 deletions test/OrchardCore.Tests/Apis/GraphQL/ContentItemsFieldTypeTests.cs
Expand Up @@ -8,6 +8,7 @@
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json.Linq;
using OrchardCore.Apis.GraphQL;
using OrchardCore.Apis.GraphQL.Queries;
using OrchardCore.ContentManagement;
using OrchardCore.ContentManagement.GraphQL.Queries;
using OrchardCore.ContentManagement.Records;
Expand Down Expand Up @@ -200,8 +201,11 @@ public async Task ShouldFilterPartsWithoutAPrefixWhenThePartHasNoPrefix()
services.Services.AddScoped<IIndexAliasProvider, MultipleAliasIndexProvider>();
services.Build();

var retrunType = new ListGraphType<StringGraphType>();
retrunType.ResolvedType = new StringGraphType() { Name = "Animal" };
var returnType = new ListGraphType<StringGraphType>();
returnType.ResolvedType = new StringGraphType() { Name = "Animal" };

var animalWhereInput = new AnimalPartWhereInput();
var inputs = new FieldType { Name = "Inputs", Arguments = new QueryArguments { new QueryArgument<WhereInputObjectGraphType> { Name = "where", Description = "filters the animals", ResolvedType = animalWhereInput } } };

var context = new ResolveFieldContext
{
Expand All @@ -210,7 +214,8 @@ public async Task ShouldFilterPartsWithoutAPrefixWhenThePartHasNoPrefix()
{
ServiceProvider = services
},
ReturnType = retrunType
ReturnType = returnType,
FieldDefinition = inputs
};

var ci = new ContentItem { ContentType = "Animal", Published = true, ContentItemId = "1", ContentItemVersionId = "1" };
Expand All @@ -229,6 +234,85 @@ public async Task ShouldFilterPartsWithoutAPrefixWhenThePartHasNoPrefix()
Assert.Equal("doug", dogs.First().As<AnimalPart>().Name);
}
}

[Fact]
public async Task ShouldFilterByCollapsedWhereInputForCollapsedParts()
{
_store.RegisterIndexes<AnimalIndexProvider>();

using (var services = new FakeServiceCollection())
{
services.Populate(new ServiceCollection());
services.Services.AddScoped(x => _store.CreateSession());
services.Services.AddScoped<IIndexProvider, ContentItemIndexProvider>();
services.Services.AddScoped<IIndexProvider, AnimalIndexProvider>();
services.Services.AddScoped<IIndexAliasProvider, MultipleAliasIndexProvider>();
services.Build();

var returnType = new ListGraphType<StringGraphType>();
returnType.ResolvedType = new StringGraphType() { Name = "Animal" };

var animalWhereInput = new AnimalPartCollapsedWhereInput();

var context = new ResolveFieldContext
{
Arguments = new Dictionary<string, object>(),
UserContext = new GraphQLContext
{
ServiceProvider = services
},
ReturnType = returnType,
FieldDefinition = new FieldType
{
Name = "Inputs",
Arguments = new QueryArguments
{
new QueryArgument<WhereInputObjectGraphType>
{
Name = "where",
Description = "filters the animals",
ResolvedType = animalWhereInput
}
}
}
};

var ci = new ContentItem { ContentType = "Animal", Published = true, ContentItemId = "1", ContentItemVersionId = "1" };
ci.Weld(new AnimalPart { Name = "doug" });

var session = ((GraphQLContext)context.UserContext).ServiceProvider.GetService<ISession>();
session.Save(ci);
await session.CommitAsync();

var type = new ContentItemsFieldType("Animal", new Schema());

context.Arguments["where"] = JObject.Parse("{ name: \"doug\" }");
var dogs = await ((AsyncFieldResolver<IEnumerable<ContentItem>>)type.Resolver).Resolve(context);

Assert.Single(dogs);
Assert.Equal("doug", dogs.First().As<AnimalPart>().Name);
}
}
}

public class AnimalPartWhereInput : WhereInputObjectGraphType
{
public AnimalPartWhereInput()
{
Name = "Test";
Description = "Foo";
AddField(new FieldType { Name = "Animal", Type = typeof(StringGraphType), Metadata = new Dictionary<string, object> { { "PartName", "AnimalPart" } } });
}
}

public class AnimalPartCollapsedWhereInput : WhereInputObjectGraphType
{
public AnimalPartCollapsedWhereInput()
{
Name = "Test";
Description = "Foo";
AddField(new FieldType { Name = "Name", Type = typeof(StringGraphType), Metadata = new Dictionary<string, object> { { "PartName", "AnimalPart" }, { "PartCollapsed", true } } });
}
}

public class Animal : ContentPart
Expand Down

0 comments on commit 1e4e28d

Please sign in to comment.