Skip to content

Commit 5c90d41

Browse files
feat: ✨ add support for filtering in primary resource with included relationships
1 parent d22466d commit 5c90d41

7 files changed

Lines changed: 461 additions & 43 deletions

File tree

JsonApiToolkit.Tests/Extensions/IncludeFilterParserTests.cs

Lines changed: 70 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public void SeparateIncludeFilters_WithOnlyMainFilters_ReturnsMainFilters()
6464
[Fact]
6565
public void SeparateIncludeFilters_WithSimpleIncludeFilter_SeparatesCorrectly()
6666
{
67-
// Arrange
67+
// Arrange - Include filters must have IsIncludeFilter=true (set by parser for bracket syntax)
6868
var filters = new FilterGroup
6969
{
7070
Filters = new List<FilterParameter>
@@ -80,6 +80,7 @@ public void SeparateIncludeFilters_WithSimpleIncludeFilter_SeparatesCorrectly()
8080
Field = "comments.status",
8181
Operator = FilterOperator.Eq,
8282
Value = "approved",
83+
IsIncludeFilter = true, // Bracket syntax: filter[comments][status][eq]=approved
8384
},
8485
},
8586
};
@@ -103,10 +104,51 @@ public void SeparateIncludeFilters_WithSimpleIncludeFilter_SeparatesCorrectly()
103104
Assert.Equal("approved", includeFilters[0].FilterGroup.Filters[0].Value);
104105
}
105106

107+
[Fact]
108+
public void SeparateIncludeFilters_WithDotNotation_TreatedAsPrimaryFilter()
109+
{
110+
// Arrange - Dot notation filters are primary filters (filter main resource through relationship)
111+
var filters = new FilterGroup
112+
{
113+
Filters = new List<FilterParameter>
114+
{
115+
new()
116+
{
117+
Field = "title",
118+
Operator = FilterOperator.Eq,
119+
Value = "Test",
120+
},
121+
new()
122+
{
123+
Field = "comments.status", // Dot notation without IsIncludeFilter = primary filter
124+
Operator = FilterOperator.Eq,
125+
Value = "approved",
126+
IsIncludeFilter = false,
127+
},
128+
},
129+
};
130+
var includePaths = new List<string> { "comments" };
131+
132+
// Act
133+
var (mainFilters, includeFilters) = IncludeFilterParser.SeparateIncludeFilters(
134+
filters,
135+
includePaths
136+
);
137+
138+
// Assert - Both filters should be main filters (dot notation = primary filter)
139+
Assert.NotNull(mainFilters);
140+
Assert.Equal(2, mainFilters.Filters.Count);
141+
Assert.Contains(mainFilters.Filters, f => f.Field == "title");
142+
Assert.Contains(mainFilters.Filters, f => f.Field == "comments.status");
143+
144+
// No include filters - dot notation is now primary filter
145+
Assert.Empty(includeFilters);
146+
}
147+
106148
[Fact]
107149
public void SeparateIncludeFilters_WithKebabCaseInclude_HandlesCorrectly()
108150
{
109-
// Arrange
151+
// Arrange - Include filters must have IsIncludeFilter=true
110152
var filters = new FilterGroup
111153
{
112154
Filters = new List<FilterParameter>
@@ -116,6 +158,7 @@ public void SeparateIncludeFilters_WithKebabCaseInclude_HandlesCorrectly()
116158
Field = "cveComments.companyCode",
117159
Operator = FilterOperator.Eq,
118160
Value = "AA",
161+
IsIncludeFilter = true, // Bracket syntax: filter[cveComments][companyCode][eq]=AA
119162
},
120163
},
121164
};
@@ -137,7 +180,7 @@ public void SeparateIncludeFilters_WithKebabCaseInclude_HandlesCorrectly()
137180
[Fact]
138181
public void SeparateIncludeFilters_WithNestedIncludeFilter_SeparatesCorrectly()
139182
{
140-
// Arrange
183+
// Arrange - Include filters must have IsIncludeFilter=true
141184
var filters = new FilterGroup
142185
{
143186
Filters = new List<FilterParameter>
@@ -147,6 +190,7 @@ public void SeparateIncludeFilters_WithNestedIncludeFilter_SeparatesCorrectly()
147190
Field = "comments.author.department",
148191
Operator = FilterOperator.Eq,
149192
Value = "Security",
193+
IsIncludeFilter = true, // Bracket syntax for nested: filter[comments.author][department][eq]=Security
150194
},
151195
},
152196
};
@@ -168,7 +212,7 @@ public void SeparateIncludeFilters_WithNestedIncludeFilter_SeparatesCorrectly()
168212
[Fact]
169213
public void SeparateIncludeFilters_WithComplexOrFilter_HandlesCorrectly()
170214
{
171-
// Arrange
215+
// Arrange - Include filters must have IsIncludeFilter=true
172216
var filters = new FilterGroup
173217
{
174218
LogicalOperator = LogicalOperator.Or,
@@ -179,12 +223,14 @@ public void SeparateIncludeFilters_WithComplexOrFilter_HandlesCorrectly()
179223
Field = "comments.companyCode",
180224
Operator = FilterOperator.Eq,
181225
Value = "AA",
226+
IsIncludeFilter = true,
182227
},
183228
new()
184229
{
185230
Field = "comments.companyCode",
186231
Operator = FilterOperator.IsNull,
187232
Value = "true",
233+
IsIncludeFilter = true,
188234
},
189235
},
190236
};
@@ -201,13 +247,16 @@ public void SeparateIncludeFilters_WithComplexOrFilter_HandlesCorrectly()
201247
Assert.Equal("comments", includeFilters[0].RelationshipPath);
202248
Assert.Equal(LogicalOperator.Or, includeFilters[0].FilterGroup.LogicalOperator);
203249
Assert.Equal(2, includeFilters[0].FilterGroup.Filters.Count);
204-
Assert.All(includeFilters[0].FilterGroup.Filters, f => Assert.Equal("companyCode", f.Field));
250+
Assert.All(
251+
includeFilters[0].FilterGroup.Filters,
252+
f => Assert.Equal("companyCode", f.Field)
253+
);
205254
}
206255

207256
[Fact]
208257
public void SeparateIncludeFilters_WithFilterOnNonIncludedRelationship_ReturnsAsMainFilter()
209258
{
210-
// Arrange
259+
// Arrange - Even with IsIncludeFilter=true, if relationship is not included, it becomes main filter
211260
var filters = new FilterGroup
212261
{
213262
Filters = new List<FilterParameter>
@@ -217,6 +266,7 @@ public void SeparateIncludeFilters_WithFilterOnNonIncludedRelationship_ReturnsAs
217266
Field = "comments.status",
218267
Operator = FilterOperator.Eq,
219268
Value = "approved",
269+
IsIncludeFilter = true, // Marked as include filter but relationship not in includes
220270
},
221271
},
222272
};
@@ -229,7 +279,7 @@ public void SeparateIncludeFilters_WithFilterOnNonIncludedRelationship_ReturnsAs
229279
);
230280

231281
// Assert
232-
// When the relationship is not included, the filter should be treated as a main filter with dot notation
282+
// When the relationship is not included, the filter should be treated as a main filter
233283
Assert.NotNull(mainFilters);
234284
Assert.Single(mainFilters.Filters);
235285
Assert.Equal("comments.status", mainFilters.Filters[0].Field);
@@ -272,7 +322,7 @@ public void SeparateIncludeFilters_WithTooManyOrConditions_ThrowsException()
272322
[Fact]
273323
public void SeparateIncludeFilters_WithTooDeepNesting_ThrowsException()
274324
{
275-
// Arrange
325+
// Arrange - Include filters must have IsIncludeFilter=true for depth checking
276326
var filters = new FilterGroup
277327
{
278328
Filters = new List<FilterParameter>
@@ -282,7 +332,8 @@ public void SeparateIncludeFilters_WithTooDeepNesting_ThrowsException()
282332
Field = "a.b.c.d.e",
283333
Operator = FilterOperator.Eq,
284334
Value = "test",
285-
}, // 5 levels deep
335+
IsIncludeFilter = true, // 5 levels deep
336+
},
286337
},
287338
};
288339
var includePaths = new List<string> { "a.b.c.d" };
@@ -298,7 +349,7 @@ public void SeparateIncludeFilters_WithTooDeepNesting_ThrowsException()
298349
[Fact]
299350
public void SeparateIncludeFilters_WithMixedMainAndIncludeFilters_SeparatesCorrectly()
300351
{
301-
// Arrange
352+
// Arrange - Include filters must have IsIncludeFilter=true
302353
var filters = new FilterGroup
303354
{
304355
Filters = new List<FilterParameter>
@@ -314,6 +365,7 @@ public void SeparateIncludeFilters_WithMixedMainAndIncludeFilters_SeparatesCorre
314365
Field = "comments.approved",
315366
Operator = FilterOperator.Eq,
316367
Value = "true",
368+
IsIncludeFilter = true, // Bracket syntax: filter[comments][approved][eq]=true
317369
},
318370
new()
319371
{
@@ -346,7 +398,7 @@ public void SeparateIncludeFilters_WithMixedMainAndIncludeFilters_SeparatesCorre
346398
[Fact]
347399
public void SeparateIncludeFilters_WithNestedGroups_HandlesCorrectly()
348400
{
349-
// Arrange
401+
// Arrange - Include filters must have IsIncludeFilter=true
350402
var filters = new FilterGroup
351403
{
352404
LogicalOperator = LogicalOperator.And,
@@ -371,12 +423,14 @@ public void SeparateIncludeFilters_WithNestedGroups_HandlesCorrectly()
371423
Field = "comments.status",
372424
Operator = FilterOperator.Eq,
373425
Value = "approved",
426+
IsIncludeFilter = true,
374427
},
375428
new()
376429
{
377430
Field = "comments.status",
378431
Operator = FilterOperator.Eq,
379432
Value = "pending",
433+
IsIncludeFilter = true,
380434
},
381435
},
382436
},
@@ -406,7 +460,7 @@ public void SeparateIncludeFilters_WithNestedGroups_HandlesCorrectly()
406460
[Fact]
407461
public void SeparateIncludeFilters_WithDeepNestedIncludeFilterUsingLeafName_SeparatesCorrectly()
408462
{
409-
// Arrange - This tests the scenario: include=cve,cve.cvecomments&filter[cvecomments.companyCode][eq]=AA
463+
// Arrange - This tests the scenario: include=cve,cve.cvecomments&filter[cvecomments][companyCode][eq]=AA
410464
var filters = new FilterGroup
411465
{
412466
Filters = new List<FilterParameter>
@@ -416,6 +470,7 @@ public void SeparateIncludeFilters_WithDeepNestedIncludeFilterUsingLeafName_Sepa
416470
Field = "cvecomments.companyCode",
417471
Operator = FilterOperator.Eq,
418472
Value = "AA",
473+
IsIncludeFilter = true, // Bracket syntax: filter[cvecomments][companyCode][eq]=AA
419474
},
420475
},
421476
};
@@ -449,6 +504,7 @@ public void SeparateIncludeFilters_WithDeepNestedIncludeFilterUsingKebabCase_Sep
449504
Field = "cveComments.companyCode",
450505
Operator = FilterOperator.Eq,
451506
Value = "AA",
507+
IsIncludeFilter = true, // Bracket syntax: filter[cveComments][companyCode][eq]=AA
452508
},
453509
},
454510
};
@@ -482,12 +538,14 @@ public void SeparateIncludeFilters_WithMultipleDeepNestedFilters_SeparatesCorrec
482538
Field = "author.name",
483539
Operator = FilterOperator.Eq,
484540
Value = "John",
541+
IsIncludeFilter = true, // Bracket syntax
485542
},
486543
new()
487544
{
488545
Field = "comments.status",
489546
Operator = FilterOperator.Eq,
490547
Value = "approved",
548+
IsIncludeFilter = true, // Bracket syntax
491549
},
492550
},
493551
};

0 commit comments

Comments
 (0)