Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[GraphQL] collapsed where statements fixes #3234 #3235

Merged
merged 8 commits into from Feb 27, 2019
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -0,0 +1,24 @@
using System.Collections.Generic;

namespace OrchardCore.ContentManagement.GraphQL
{
public static class FieldMetaDataExtensions
{
public static IDictionary<string, object> AddPartMetaData(this IDictionary<string, object> metaData, string partName, bool partCollapsed = false)
{
metaData = metaData ?? new Dictionary<string, object>();

if (!metaData.ContainsKey("PartCollapsed"))
{
metaData.Add("PartCollapsed", partCollapsed);
}

if (!metaData.ContainsKey("PartName"))
{
metaData.Add("PartName", partName);
}

return metaData;
}
}
}
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>();

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.ToLower(), alias.Alias);
carlwoodhouse marked this conversation as resolved.
Show resolved Hide resolved

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.ToLower(), 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
Expand Up @@ -53,7 +53,7 @@ public void Build(FieldType contentQuery, ContentTypeDefinition contentTypeDefin
if (fieldType != null)
{
if (_contentOptions.ShouldSkip(fieldType.Type, fieldType.Name)) continue;

fieldType.Metadata = fieldType.Metadata.AddPartMetaData(part.PartDefinition.Name, true);
contentItemType.AddField(fieldType);
break;
}
Expand All @@ -74,6 +74,7 @@ public void Build(FieldType contentQuery, ContentTypeDefinition contentTypeDefin
return context.Source.Get(typeToResolve, nameToResolve);
});

field.Metadata = field.Metadata.AddPartMetaData(part.PartDefinition.Name);
field.ResolvedType = new DynamicPartGraphType(_httpContextAccessor, part);
}
}
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,27 @@ public void Build(FieldType contentQuery, ContentTypeDefinition contentTypeDefin

var whereInput = (ContentItemWhereInput)whereArgument.ResolvedType;

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

Description = inputGraphTypeResolved.Description
});
var field = new FieldType
{
Type = inputGraphTypeResolved.GetType(),
Name = partName.ToFieldName(),
Description = inputGraphTypeResolved.Description
};

field.Metadata = field.Metadata.AddPartMetaData(partName);

whereInput.AddField(field);
}
}
}
}
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