Skip to content

Commit dd58180

Browse files
Merge pull request #28 from intility/or-and-grouping-issues
2 parents c352665 + 86cab81 commit dd58180

7 files changed

Lines changed: 211 additions & 132 deletions

File tree

JsonApiToolkit.Tests/Extensions/FilteredIncludeBuilderTests.cs

Lines changed: 39 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,18 @@ public void ApplyFilteredIncludes_WithSimpleIncludeFilter_BuildsCorrectExpressio
6565
new()
6666
{
6767
RelationshipPath = "comments",
68-
FieldPath = "status",
69-
Filter = new FilterParameter
68+
FilterGroup = new FilterGroup
7069
{
71-
Field = "status",
72-
Operator = FilterOperator.Eq,
73-
Value = "approved",
74-
},
70+
Filters = new List<FilterParameter>
71+
{
72+
new()
73+
{
74+
Field = "status",
75+
Operator = FilterOperator.Eq,
76+
Value = "approved",
77+
}
78+
}
79+
}
7580
},
7681
};
7782

@@ -94,24 +99,24 @@ public void ApplyFilteredIncludes_WithMultipleFiltersOnSameRelationship_Combines
9499
new()
95100
{
96101
RelationshipPath = "comments",
97-
FieldPath = "status",
98-
Filter = new FilterParameter
102+
FilterGroup = new FilterGroup
99103
{
100-
Field = "status",
101-
Operator = FilterOperator.Eq,
102-
Value = "approved",
103-
},
104-
},
105-
new()
106-
{
107-
RelationshipPath = "comments",
108-
FieldPath = "priority",
109-
Filter = new FilterParameter
110-
{
111-
Field = "priority",
112-
Operator = FilterOperator.Gt,
113-
Value = "5",
114-
},
104+
Filters = new List<FilterParameter>
105+
{
106+
new()
107+
{
108+
Field = "status",
109+
Operator = FilterOperator.Eq,
110+
Value = "approved",
111+
},
112+
new()
113+
{
114+
Field = "priority",
115+
Operator = FilterOperator.Gt,
116+
Value = "5",
117+
}
118+
}
119+
}
115120
},
116121
};
117122

@@ -148,13 +153,18 @@ public void ApplyFilteredIncludes_WithMixedFilteredAndUnfilteredIncludes_Handles
148153
new()
149154
{
150155
RelationshipPath = "comments",
151-
FieldPath = "status",
152-
Filter = new FilterParameter
156+
FilterGroup = new FilterGroup
153157
{
154-
Field = "status",
155-
Operator = FilterOperator.Eq,
156-
Value = "approved",
157-
},
158+
Filters = new List<FilterParameter>
159+
{
160+
new()
161+
{
162+
Field = "status",
163+
Operator = FilterOperator.Eq,
164+
Value = "approved",
165+
}
166+
}
167+
}
158168
},
159169
// tags and author have no filters
160170
};

JsonApiToolkit.Tests/Extensions/IncludeFilterParserTests.cs

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,9 @@ public void SeparateIncludeFilters_WithSimpleIncludeFilter_SeparatesCorrectly()
9898

9999
Assert.Single(includeFilters);
100100
Assert.Equal("comments", includeFilters[0].RelationshipPath);
101-
Assert.Equal("status", includeFilters[0].FieldPath);
102-
Assert.Equal("approved", includeFilters[0].Filter.Value);
101+
Assert.Single(includeFilters[0].FilterGroup.Filters);
102+
Assert.Equal("status", includeFilters[0].FilterGroup.Filters[0].Field);
103+
Assert.Equal("approved", includeFilters[0].FilterGroup.Filters[0].Value);
103104
}
104105

105106
[Fact]
@@ -129,7 +130,8 @@ public void SeparateIncludeFilters_WithKebabCaseInclude_HandlesCorrectly()
129130
// Assert
130131
Assert.Single(includeFilters);
131132
Assert.Equal("cveComments", includeFilters[0].RelationshipPath);
132-
Assert.Equal("companyCode", includeFilters[0].FieldPath);
133+
Assert.Single(includeFilters[0].FilterGroup.Filters);
134+
Assert.Equal("companyCode", includeFilters[0].FilterGroup.Filters[0].Field);
133135
}
134136

135137
[Fact]
@@ -159,7 +161,8 @@ public void SeparateIncludeFilters_WithNestedIncludeFilter_SeparatesCorrectly()
159161
// Assert
160162
Assert.Single(includeFilters);
161163
Assert.Equal("comments.author", includeFilters[0].RelationshipPath);
162-
Assert.Equal("department", includeFilters[0].FieldPath);
164+
Assert.Single(includeFilters[0].FilterGroup.Filters);
165+
Assert.Equal("department", includeFilters[0].FilterGroup.Filters[0].Field);
163166
}
164167

165168
[Fact]
@@ -194,9 +197,11 @@ public void SeparateIncludeFilters_WithComplexOrFilter_HandlesCorrectly()
194197
);
195198

196199
// Assert
197-
Assert.Equal(2, includeFilters.Count);
198-
Assert.All(includeFilters, f => Assert.Equal("comments", f.RelationshipPath));
199-
Assert.All(includeFilters, f => Assert.Equal("companyCode", f.FieldPath));
200+
Assert.Single(includeFilters);
201+
Assert.Equal("comments", includeFilters[0].RelationshipPath);
202+
Assert.Equal(LogicalOperator.Or, includeFilters[0].FilterGroup.LogicalOperator);
203+
Assert.Equal(2, includeFilters[0].FilterGroup.Filters.Count);
204+
Assert.All(includeFilters[0].FilterGroup.Filters, f => Assert.Equal("companyCode", f.Field));
200205
}
201206

202207
[Fact]
@@ -334,7 +339,8 @@ public void SeparateIncludeFilters_WithMixedMainAndIncludeFilters_SeparatesCorre
334339

335340
Assert.Single(includeFilters);
336341
Assert.Equal("comments", includeFilters[0].RelationshipPath);
337-
Assert.Equal("approved", includeFilters[0].FieldPath);
342+
Assert.Single(includeFilters[0].FilterGroup.Filters);
343+
Assert.Equal("approved", includeFilters[0].FilterGroup.Filters[0].Field);
338344
}
339345

340346
[Fact]
@@ -389,9 +395,12 @@ public void SeparateIncludeFilters_WithNestedGroups_HandlesCorrectly()
389395
Assert.Single(mainFilters.Filters);
390396
Assert.Equal("title", mainFilters.Filters[0].Field);
391397

392-
Assert.Equal(2, includeFilters.Count);
393-
Assert.All(includeFilters, f => Assert.Equal("comments", f.RelationshipPath));
394-
Assert.All(includeFilters, f => Assert.Equal("status", f.FieldPath));
398+
Assert.Single(includeFilters);
399+
Assert.Equal("comments", includeFilters[0].RelationshipPath);
400+
// The OR group should be used directly (not wrapped)
401+
Assert.Equal(LogicalOperator.Or, includeFilters[0].FilterGroup.LogicalOperator);
402+
Assert.Equal(2, includeFilters[0].FilterGroup.Filters.Count);
403+
Assert.All(includeFilters[0].FilterGroup.Filters, f => Assert.Equal("status", f.Field));
395404
}
396405

397406
[Fact]
@@ -422,8 +431,9 @@ public void SeparateIncludeFilters_WithDeepNestedIncludeFilterUsingLeafName_Sepa
422431
Assert.Null(mainFilters); // No main filters expected
423432
Assert.Single(includeFilters);
424433
Assert.Equal("cve.cvecomments", includeFilters[0].RelationshipPath);
425-
Assert.Equal("companyCode", includeFilters[0].FieldPath);
426-
Assert.Equal("AA", includeFilters[0].Filter.Value);
434+
Assert.Single(includeFilters[0].FilterGroup.Filters);
435+
Assert.Equal("companyCode", includeFilters[0].FilterGroup.Filters[0].Field);
436+
Assert.Equal("AA", includeFilters[0].FilterGroup.Filters[0].Value);
427437
}
428438

429439
[Fact]
@@ -455,7 +465,8 @@ public void SeparateIncludeFilters_WithDeepNestedIncludeFilterUsingKebabCase_Sep
455465
Assert.Single(includeFilters);
456466
// The relationship path is returned as the matched normalized path
457467
Assert.Equal("cve.cveComments", includeFilters[0].RelationshipPath);
458-
Assert.Equal("companyCode", includeFilters[0].FieldPath);
468+
Assert.Single(includeFilters[0].FilterGroup.Filters);
469+
Assert.Equal("companyCode", includeFilters[0].FilterGroup.Filters[0].Field);
459470
}
460471

461472
[Fact]
@@ -492,12 +503,16 @@ public void SeparateIncludeFilters_WithMultipleDeepNestedFilters_SeparatesCorrec
492503
Assert.Null(mainFilters);
493504
Assert.Equal(2, includeFilters.Count);
494505

495-
var authorFilter = includeFilters.First(f => f.FieldPath == "name");
506+
var authorFilter = includeFilters.First(f => f.RelationshipPath == "posts.author");
496507
Assert.Equal("posts.author", authorFilter.RelationshipPath);
497-
Assert.Equal("John", authorFilter.Filter.Value);
508+
Assert.Single(authorFilter.FilterGroup.Filters);
509+
Assert.Equal("name", authorFilter.FilterGroup.Filters[0].Field);
510+
Assert.Equal("John", authorFilter.FilterGroup.Filters[0].Value);
498511

499-
var commentFilter = includeFilters.First(f => f.FieldPath == "status");
512+
var commentFilter = includeFilters.First(f => f.RelationshipPath == "posts.comments");
500513
Assert.Equal("posts.comments", commentFilter.RelationshipPath);
501-
Assert.Equal("approved", commentFilter.Filter.Value);
514+
Assert.Single(commentFilter.FilterGroup.Filters);
515+
Assert.Equal("status", commentFilter.FilterGroup.Filters[0].Field);
516+
Assert.Equal("approved", commentFilter.FilterGroup.Filters[0].Value);
502517
}
503518
}

JsonApiToolkit/Extensions/Querying/Filtering/FilterExpressionBuilder.cs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,19 @@ public static class FilterExpressionBuilder
1818
ParameterExpression parameter,
1919
ILogger? logger = null
2020
)
21+
{
22+
return BuildFilterExpression(group, parameter, typeof(T), logger);
23+
}
24+
25+
/// <summary>
26+
/// Builds a composite filter expression from filter conditions and nested groups (non-generic overload).
27+
/// </summary>
28+
public static Expression? BuildFilterExpression(
29+
FilterGroup group,
30+
ParameterExpression parameter,
31+
Type entityType,
32+
ILogger? logger = null
33+
)
2134
{
2235
var expressions = new List<Expression>();
2336

@@ -31,15 +44,15 @@ public static class FilterExpressionBuilder
3144
else
3245
{
3346
PropertyInfo? property = QueryHelpers.GetPropertyByJsonName(
34-
typeof(T),
47+
entityType,
3548
filter.Field
3649
);
3750
if (property == null)
3851
{
3952
logger?.LogWarning(
4053
"Property '{Field}' not found on {Type}, skipping filter",
4154
filter.Field,
42-
typeof(T).Name
55+
entityType.Name
4356
);
4457
continue;
4558
}
@@ -58,7 +71,7 @@ public static class FilterExpressionBuilder
5871

5972
foreach (FilterGroup nestedGroup in group.Groups)
6073
{
61-
Expression? nestedExpr = BuildFilterExpression<T>(nestedGroup, parameter, logger);
74+
Expression? nestedExpr = BuildFilterExpression(nestedGroup, parameter, entityType, logger);
6275
if (nestedExpr != null)
6376
expressions.Add(nestedExpr);
6477
}

0 commit comments

Comments
 (0)