From 16349dab8a4aa34d13f4af6cc9c60b9385d29223 Mon Sep 17 00:00:00 2001 From: Paul DeVito Date: Thu, 1 Oct 2026 17:31:08 +0300 Subject: [PATCH] fix(filter)!: ignore a fully prevented clause by its query name v1.14.2 and main throw InvalidOperationException when a filter uses the query name of a property with PreventFilter and PreventSort. The member name of the same property gives an ignored clause. The alias rewrite pass no longer throws, so the query name gives the same ignored clause as the member name. BREAKING CHANGE: a filter clause on the query name of a property with PreventFilter and PreventSort no longer throws InvalidOperationException. IgnoredClauseBehavior controls the clause, like for the member name. QueryKitPropertyMappings.ReplaceAliasesWithPropertyPaths no longer throws for this query name. --- .../Tests/PropertyResolverTests.cs | 29 +++++++++++++++++++ QueryKit.UnitTests/PropertyResolverTests.cs | 27 +++++++++++++++++ QueryKit/QueryKitPropertyMappings.cs | 5 ---- 3 files changed, 56 insertions(+), 5 deletions(-) diff --git a/QueryKit.IntegrationTests/Tests/PropertyResolverTests.cs b/QueryKit.IntegrationTests/Tests/PropertyResolverTests.cs index ac5a26f..2c2112b 100644 --- a/QueryKit.IntegrationTests/Tests/PropertyResolverTests.cs +++ b/QueryKit.IntegrationTests/Tests/PropertyResolverTests.cs @@ -443,6 +443,35 @@ public async Task query_name_in_arithmetic_is_filtered() people.Should().ContainSingle(x => x.Id == fakePerson.Id); } + [Fact] + public async Task prevented_property_clause_by_its_query_name_under_or_does_not_return_every_row() + { + // Arrange + var testingServiceScope = new TestingServiceScope(); + var title = new Faker().Lorem.Sentence(); + var fakePerson = new FakeTestingPersonBuilder() + .WithTitle(title) + .WithFirstName("Paul") + .WithAge(30) + .Build(); + await testingServiceScope.InsertAsync(fakePerson); + + var input = $"""Title == "{title}" && (first == "Paul" || Age > 100)"""; + var config = new QueryKitConfiguration(config => + { + config.IgnoredClauseBehavior = IgnoredClauseBehavior.Remove; + config.Property(x => x.FirstName!).HasQueryName("first").PreventFilter().PreventSort(); + }); + + // Act + var people = await testingServiceScope.DbContext().People + .ApplyQueryKitFilter(input, config) + .ToListAsync(); + + // Assert + people.Should().BeEmpty(); + } + [Theory] [InlineData("first-name")] [InlineData("_first")] diff --git a/QueryKit.UnitTests/PropertyResolverTests.cs b/QueryKit.UnitTests/PropertyResolverTests.cs index 0d460b6..d741e84 100644 --- a/QueryKit.UnitTests/PropertyResolverTests.cs +++ b/QueryKit.UnitTests/PropertyResolverTests.cs @@ -770,6 +770,33 @@ public void property_prevented_for_filter_and_sort_is_removed_by_its_query_name_ filterExpression.ToDisplayString().Should().Be("x => (x.Age > 100)"); } + [Fact] + public void query_name_of_a_property_prevented_for_filter_and_sort_in_a_quoted_value_is_not_changed() + { + var input = """Title == "name == x" """; + var config = new QueryKitConfiguration(config => + { + config.Property(x => x.FirstName!).HasQueryName("name").PreventFilter().PreventSort(); + }); + + var filterExpression = FilterParser.ParseFilter(input, config); + + filterExpression.ToDisplayString().Should().Be("""x => (x.Title == "name == x")"""); + } + + [Fact] + public void alias_replacement_does_not_throw_for_a_property_prevented_for_filter_and_sort() + { + var config = new QueryKitConfiguration(config => + { + config.Property(x => x.Title!).HasQueryName("name").PreventFilter().PreventSort(); + }); + + var input = config.PropertyMappings.ReplaceAliasesWithPropertyPaths("""name == "x" """); + + input.Should().Be("""Title == "x" """); + } + [Theory] [InlineData("==")] [InlineData("_=")] diff --git a/QueryKit/QueryKitPropertyMappings.cs b/QueryKit/QueryKitPropertyMappings.cs index d914ca8..16ecdf8 100644 --- a/QueryKit/QueryKitPropertyMappings.cs +++ b/QueryKit/QueryKitPropertyMappings.cs @@ -153,11 +153,6 @@ public string ReplaceAliasesWithPropertyPaths(string input) // (e.g. the `^` in `^^` would otherwise be read as a start-of-line anchor). var regex = AliasRegexCache.Get($@"\b{Regex.Escape(queryKitPropertyInfo.QueryName!)}\b(?=\s*{Regex.Escape(op)})"); - if (queryKitPropertyInfo is { CanSort: false, CanFilter: false} && regex.IsMatch(input)) - { - throw new InvalidOperationException($"'{queryKitPropertyInfo.Name}' is not allowed for filtering or sorting."); - } - input = regex.Replace(input, propertyPath); } }