Skip to content

Commit 0b0bb87

Browse files
Merge pull request #39 from intility/relationship-primary-filter
feat: ✨ add support for filtering in primary resource with included r…
2 parents 65107d5 + 20b3598 commit 0b0bb87

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);
@@ -239,7 +289,7 @@ public void SeparateIncludeFilters_WithFilterOnNonIncludedRelationship_ReturnsAs
239289
[Fact]
240290
public void SeparateIncludeFilters_WithTooDeepNesting_ThrowsException()
241291
{
242-
// Arrange
292+
// Arrange - Include filters must have IsIncludeFilter=true for depth checking
243293
var filters = new FilterGroup
244294
{
245295
Filters = new List<FilterParameter>
@@ -249,7 +299,8 @@ public void SeparateIncludeFilters_WithTooDeepNesting_ThrowsException()
249299
Field = "a.b.c.d.e",
250300
Operator = FilterOperator.Eq,
251301
Value = "test",
252-
}, // 5 levels deep
302+
IsIncludeFilter = true, // 5 levels deep
303+
},
253304
},
254305
};
255306
var includePaths = new List<string> { "a.b.c.d" };
@@ -265,7 +316,7 @@ public void SeparateIncludeFilters_WithTooDeepNesting_ThrowsException()
265316
[Fact]
266317
public void SeparateIncludeFilters_WithMixedMainAndIncludeFilters_SeparatesCorrectly()
267318
{
268-
// Arrange
319+
// Arrange - Include filters must have IsIncludeFilter=true
269320
var filters = new FilterGroup
270321
{
271322
Filters = new List<FilterParameter>
@@ -281,6 +332,7 @@ public void SeparateIncludeFilters_WithMixedMainAndIncludeFilters_SeparatesCorre
281332
Field = "comments.approved",
282333
Operator = FilterOperator.Eq,
283334
Value = "true",
335+
IsIncludeFilter = true, // Bracket syntax: filter[comments][approved][eq]=true
284336
},
285337
new()
286338
{
@@ -313,7 +365,7 @@ public void SeparateIncludeFilters_WithMixedMainAndIncludeFilters_SeparatesCorre
313365
[Fact]
314366
public void SeparateIncludeFilters_WithNestedGroups_HandlesCorrectly()
315367
{
316-
// Arrange
368+
// Arrange - Include filters must have IsIncludeFilter=true
317369
var filters = new FilterGroup
318370
{
319371
LogicalOperator = LogicalOperator.And,
@@ -338,12 +390,14 @@ public void SeparateIncludeFilters_WithNestedGroups_HandlesCorrectly()
338390
Field = "comments.status",
339391
Operator = FilterOperator.Eq,
340392
Value = "approved",
393+
IsIncludeFilter = true,
341394
},
342395
new()
343396
{
344397
Field = "comments.status",
345398
Operator = FilterOperator.Eq,
346399
Value = "pending",
400+
IsIncludeFilter = true,
347401
},
348402
},
349403
},
@@ -373,7 +427,7 @@ public void SeparateIncludeFilters_WithNestedGroups_HandlesCorrectly()
373427
[Fact]
374428
public void SeparateIncludeFilters_WithDeepNestedIncludeFilterUsingLeafName_SeparatesCorrectly()
375429
{
376-
// Arrange - This tests the scenario: include=cve,cve.cvecomments&filter[cvecomments.companyCode][eq]=AA
430+
// Arrange - This tests the scenario: include=cve,cve.cvecomments&filter[cvecomments][companyCode][eq]=AA
377431
var filters = new FilterGroup
378432
{
379433
Filters = new List<FilterParameter>
@@ -383,6 +437,7 @@ public void SeparateIncludeFilters_WithDeepNestedIncludeFilterUsingLeafName_Sepa
383437
Field = "cvecomments.companyCode",
384438
Operator = FilterOperator.Eq,
385439
Value = "AA",
440+
IsIncludeFilter = true, // Bracket syntax: filter[cvecomments][companyCode][eq]=AA
386441
},
387442
},
388443
};
@@ -416,6 +471,7 @@ public void SeparateIncludeFilters_WithDeepNestedIncludeFilterUsingKebabCase_Sep
416471
Field = "cveComments.companyCode",
417472
Operator = FilterOperator.Eq,
418473
Value = "AA",
474+
IsIncludeFilter = true, // Bracket syntax: filter[cveComments][companyCode][eq]=AA
419475
},
420476
},
421477
};
@@ -449,12 +505,14 @@ public void SeparateIncludeFilters_WithMultipleDeepNestedFilters_SeparatesCorrec
449505
Field = "author.name",
450506
Operator = FilterOperator.Eq,
451507
Value = "John",
508+
IsIncludeFilter = true, // Bracket syntax
452509
},
453510
new()
454511
{
455512
Field = "comments.status",
456513
Operator = FilterOperator.Eq,
457514
Value = "approved",
515+
IsIncludeFilter = true, // Bracket syntax
458516
},
459517
},
460518
};

0 commit comments

Comments
 (0)