Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 39 additions & 29 deletions JsonApiToolkit.Tests/Extensions/FilteredIncludeBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,18 @@ public void ApplyFilteredIncludes_WithSimpleIncludeFilter_BuildsCorrectExpressio
new()
{
RelationshipPath = "comments",
FieldPath = "status",
Filter = new FilterParameter
FilterGroup = new FilterGroup
{
Field = "status",
Operator = FilterOperator.Eq,
Value = "approved",
},
Filters = new List<FilterParameter>
{
new()
{
Field = "status",
Operator = FilterOperator.Eq,
Value = "approved",
}
}
}
},
};

Expand All @@ -94,24 +99,24 @@ public void ApplyFilteredIncludes_WithMultipleFiltersOnSameRelationship_Combines
new()
{
RelationshipPath = "comments",
FieldPath = "status",
Filter = new FilterParameter
FilterGroup = new FilterGroup
{
Field = "status",
Operator = FilterOperator.Eq,
Value = "approved",
},
},
new()
{
RelationshipPath = "comments",
FieldPath = "priority",
Filter = new FilterParameter
{
Field = "priority",
Operator = FilterOperator.Gt,
Value = "5",
},
Filters = new List<FilterParameter>
{
new()
{
Field = "status",
Operator = FilterOperator.Eq,
Value = "approved",
},
new()
{
Field = "priority",
Operator = FilterOperator.Gt,
Value = "5",
}
}
}
},
};

Expand Down Expand Up @@ -148,13 +153,18 @@ public void ApplyFilteredIncludes_WithMixedFilteredAndUnfilteredIncludes_Handles
new()
{
RelationshipPath = "comments",
FieldPath = "status",
Filter = new FilterParameter
FilterGroup = new FilterGroup
{
Field = "status",
Operator = FilterOperator.Eq,
Value = "approved",
},
Filters = new List<FilterParameter>
{
new()
{
Field = "status",
Operator = FilterOperator.Eq,
Value = "approved",
}
}
}
},
// tags and author have no filters
};
Expand Down
51 changes: 33 additions & 18 deletions JsonApiToolkit.Tests/Extensions/IncludeFilterParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,9 @@ public void SeparateIncludeFilters_WithSimpleIncludeFilter_SeparatesCorrectly()

Assert.Single(includeFilters);
Assert.Equal("comments", includeFilters[0].RelationshipPath);
Assert.Equal("status", includeFilters[0].FieldPath);
Assert.Equal("approved", includeFilters[0].Filter.Value);
Assert.Single(includeFilters[0].FilterGroup.Filters);
Assert.Equal("status", includeFilters[0].FilterGroup.Filters[0].Field);
Assert.Equal("approved", includeFilters[0].FilterGroup.Filters[0].Value);
}

[Fact]
Expand Down Expand Up @@ -129,7 +130,8 @@ public void SeparateIncludeFilters_WithKebabCaseInclude_HandlesCorrectly()
// Assert
Assert.Single(includeFilters);
Assert.Equal("cveComments", includeFilters[0].RelationshipPath);
Assert.Equal("companyCode", includeFilters[0].FieldPath);
Assert.Single(includeFilters[0].FilterGroup.Filters);
Assert.Equal("companyCode", includeFilters[0].FilterGroup.Filters[0].Field);
}

[Fact]
Expand Down Expand Up @@ -159,7 +161,8 @@ public void SeparateIncludeFilters_WithNestedIncludeFilter_SeparatesCorrectly()
// Assert
Assert.Single(includeFilters);
Assert.Equal("comments.author", includeFilters[0].RelationshipPath);
Assert.Equal("department", includeFilters[0].FieldPath);
Assert.Single(includeFilters[0].FilterGroup.Filters);
Assert.Equal("department", includeFilters[0].FilterGroup.Filters[0].Field);
}

[Fact]
Expand Down Expand Up @@ -194,9 +197,11 @@ public void SeparateIncludeFilters_WithComplexOrFilter_HandlesCorrectly()
);

// Assert
Assert.Equal(2, includeFilters.Count);
Assert.All(includeFilters, f => Assert.Equal("comments", f.RelationshipPath));
Assert.All(includeFilters, f => Assert.Equal("companyCode", f.FieldPath));
Assert.Single(includeFilters);
Assert.Equal("comments", includeFilters[0].RelationshipPath);
Assert.Equal(LogicalOperator.Or, includeFilters[0].FilterGroup.LogicalOperator);
Assert.Equal(2, includeFilters[0].FilterGroup.Filters.Count);
Assert.All(includeFilters[0].FilterGroup.Filters, f => Assert.Equal("companyCode", f.Field));
}

[Fact]
Expand Down Expand Up @@ -334,7 +339,8 @@ public void SeparateIncludeFilters_WithMixedMainAndIncludeFilters_SeparatesCorre

Assert.Single(includeFilters);
Assert.Equal("comments", includeFilters[0].RelationshipPath);
Assert.Equal("approved", includeFilters[0].FieldPath);
Assert.Single(includeFilters[0].FilterGroup.Filters);
Assert.Equal("approved", includeFilters[0].FilterGroup.Filters[0].Field);
}

[Fact]
Expand Down Expand Up @@ -389,9 +395,12 @@ public void SeparateIncludeFilters_WithNestedGroups_HandlesCorrectly()
Assert.Single(mainFilters.Filters);
Assert.Equal("title", mainFilters.Filters[0].Field);

Assert.Equal(2, includeFilters.Count);
Assert.All(includeFilters, f => Assert.Equal("comments", f.RelationshipPath));
Assert.All(includeFilters, f => Assert.Equal("status", f.FieldPath));
Assert.Single(includeFilters);
Assert.Equal("comments", includeFilters[0].RelationshipPath);
// The OR group should be used directly (not wrapped)
Assert.Equal(LogicalOperator.Or, includeFilters[0].FilterGroup.LogicalOperator);
Assert.Equal(2, includeFilters[0].FilterGroup.Filters.Count);
Assert.All(includeFilters[0].FilterGroup.Filters, f => Assert.Equal("status", f.Field));
}

[Fact]
Expand Down Expand Up @@ -422,8 +431,9 @@ public void SeparateIncludeFilters_WithDeepNestedIncludeFilterUsingLeafName_Sepa
Assert.Null(mainFilters); // No main filters expected
Assert.Single(includeFilters);
Assert.Equal("cve.cvecomments", includeFilters[0].RelationshipPath);
Assert.Equal("companyCode", includeFilters[0].FieldPath);
Assert.Equal("AA", includeFilters[0].Filter.Value);
Assert.Single(includeFilters[0].FilterGroup.Filters);
Assert.Equal("companyCode", includeFilters[0].FilterGroup.Filters[0].Field);
Assert.Equal("AA", includeFilters[0].FilterGroup.Filters[0].Value);
}

[Fact]
Expand Down Expand Up @@ -455,7 +465,8 @@ public void SeparateIncludeFilters_WithDeepNestedIncludeFilterUsingKebabCase_Sep
Assert.Single(includeFilters);
// The relationship path is returned as the matched normalized path
Assert.Equal("cve.cveComments", includeFilters[0].RelationshipPath);
Assert.Equal("companyCode", includeFilters[0].FieldPath);
Assert.Single(includeFilters[0].FilterGroup.Filters);
Assert.Equal("companyCode", includeFilters[0].FilterGroup.Filters[0].Field);
}

[Fact]
Expand Down Expand Up @@ -492,12 +503,16 @@ public void SeparateIncludeFilters_WithMultipleDeepNestedFilters_SeparatesCorrec
Assert.Null(mainFilters);
Assert.Equal(2, includeFilters.Count);

var authorFilter = includeFilters.First(f => f.FieldPath == "name");
var authorFilter = includeFilters.First(f => f.RelationshipPath == "posts.author");
Assert.Equal("posts.author", authorFilter.RelationshipPath);
Assert.Equal("John", authorFilter.Filter.Value);
Assert.Single(authorFilter.FilterGroup.Filters);
Assert.Equal("name", authorFilter.FilterGroup.Filters[0].Field);
Assert.Equal("John", authorFilter.FilterGroup.Filters[0].Value);

var commentFilter = includeFilters.First(f => f.FieldPath == "status");
var commentFilter = includeFilters.First(f => f.RelationshipPath == "posts.comments");
Assert.Equal("posts.comments", commentFilter.RelationshipPath);
Assert.Equal("approved", commentFilter.Filter.Value);
Assert.Single(commentFilter.FilterGroup.Filters);
Assert.Equal("status", commentFilter.FilterGroup.Filters[0].Field);
Assert.Equal("approved", commentFilter.FilterGroup.Filters[0].Value);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,19 @@ public static class FilterExpressionBuilder
ParameterExpression parameter,
ILogger? logger = null
)
{
return BuildFilterExpression(group, parameter, typeof(T), logger);
}

/// <summary>
/// Builds a composite filter expression from filter conditions and nested groups (non-generic overload).
/// </summary>
public static Expression? BuildFilterExpression(
FilterGroup group,
ParameterExpression parameter,
Type entityType,
ILogger? logger = null
)
{
var expressions = new List<Expression>();

Expand All @@ -31,15 +44,15 @@ public static class FilterExpressionBuilder
else
{
PropertyInfo? property = QueryHelpers.GetPropertyByJsonName(
typeof(T),
entityType,
filter.Field
);
if (property == null)
{
logger?.LogWarning(
"Property '{Field}' not found on {Type}, skipping filter",
filter.Field,
typeof(T).Name
entityType.Name
);
continue;
}
Expand All @@ -58,7 +71,7 @@ public static class FilterExpressionBuilder

foreach (FilterGroup nestedGroup in group.Groups)
{
Expression? nestedExpr = BuildFilterExpression<T>(nestedGroup, parameter, logger);
Expression? nestedExpr = BuildFilterExpression(nestedGroup, parameter, entityType, logger);
if (nestedExpr != null)
expressions.Add(nestedExpr);
}
Expand Down
Loading