jpie 4.1.2 → 4.1.4.pre.202610110318

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7cb5c150e4da0f0608aa5b97534e2239b8b80900054c984ca2aed3c24e747198
4
- data.tar.gz: 83558900f75be4e9a4b8ea5997f7551963ee1406359b9fd30903d39fe9ad78bb
3
+ metadata.gz: 7db92a53fbf58d17736a35e3d0461902058cc3f9361ab82973cec74dcbed6bbb
4
+ data.tar.gz: df97d8efc85b0191937869915c37b23a7c90c5115cf76990db41bf6320103d82
5
5
  SHA512:
6
- metadata.gz: 1fdbf06edc4419def82b3719d3cd419e84bc76f3ebb9af0e64650c8eea9856d0d0a20c688a8c291c491fddaed0f28a417b506391dc63e3987ab8594a34126f4f
7
- data.tar.gz: cfa30d1e5be610419135bcd16b13752a9eab3792c8082fe95d4a8cc6272103c6ea62c6315bdfc3e46b68b2d7ffe8467b1231e37980e8872b0f3f3781ba71b3d0
6
+ metadata.gz: 7b74f2101c938a0044fb96a79d756d2c018b0789a080f1404b13a683e6d014be126e8f53d79a0f5aba73c2f6341e82c6a6af85afe1bd857dc8b476cad1a4c06e
7
+ data.tar.gz: b56a0a2cbb1a6f9b9541744496aa3dff7c5a8bbdb811666949af8af34e2b1217249f8f2a1ce5b2586199fe3b437045f87c63fcb1d7709ce8fc1d2354cbe048fa
data/CHANGELOG.md CHANGED
@@ -7,6 +7,27 @@ at release time.
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [4.1.3] - 2026-10-10
11
+
12
+ ### Fixed
13
+
14
+ - **Filter a polymorphic hop on the model that owns it.** A path such as
15
+ `filter[user.profile.id_eq]` on `/posts` passed validation and answered 500.
16
+ `PolymorphicFilters` read the foreign key and type columns from the root
17
+ table of the query, and `posts` holds no `profile_id`. A root table that
18
+ holds a column of the same name answered 200 with rows filtered on the wrong
19
+ table. The filter now joins the hops before the polymorphic one and reads
20
+ the columns of the owner's table. A polymorphic hop on the root model
21
+ behaves as before. Regression spec:
22
+ `nested_filter_polymorphic_hop_below_root_spec.rb`.
23
+ - **Ignore an empty nested filter value.** `filter[comments.author.email_eq]=`
24
+ added the inner join before it checked the value, so the answer held only
25
+ the posts that have a comment. `filter[profile.type]=` matched the empty
26
+ string and answered no rows. An empty nested value now adds no join and no
27
+ condition, as an empty top-level filter does. A nested filter that adds no
28
+ condition adds no join either. Regression spec:
29
+ `nested_filter_empty_value_spec.rb`.
30
+
10
31
  ## [4.1.2] - 2026-10-10
11
32
 
12
33
  ### Fixed
@@ -19,23 +19,25 @@ module JSONAPI
19
19
  def apply_filter_for_path(scope, filter_name, filter_value)
20
20
  *relationship_chain, leaf_filter = filter_name.split(".")
21
21
  return scope if relationship_chain.empty?
22
+ # An empty value filters nothing, so it adds no join. An inner join
23
+ # drops each row that has no related record.
24
+ return scope if empty_filter_value?(filter_value)
22
25
 
23
26
  # The walk filter validation takes. A path it refuses filters nothing.
24
27
  target = filter_path.resolve(filter_name)
25
28
  return scope unless target
26
29
 
27
- if target.association
28
- attributes = { leaf_filter => filter_value }
29
- return apply_polymorphic_nested_filters(scope, target.association, relationship_chain.last, attributes)
30
- end
31
-
32
- apply_joined_filter(scope, target.model, relationship_chain:, leaf_filter:, filter_value:)
30
+ # The foreign key and type columns of a polymorphic hop sit on the model
31
+ # that owns the association. Only the hops before it need a join.
32
+ joined = join_chain(scope, target.association ? relationship_chain[0..-2] : relationship_chain)
33
+ filtered = apply_filter_on_target(joined, target, leaf_filter, filter_value)
34
+ # A filter that adds no condition adds no join either.
35
+ filtered.equal?(joined) ? scope : filtered
33
36
  end
34
37
 
35
- def apply_joined_filter(scope, target_model, relationship_chain:, leaf_filter:, filter_value:)
36
- join_hash = build_join_hash_for_chain(relationship_chain)
37
- scope = scope.joins(join_hash) if join_hash.present?
38
- apply_filter_on_model(scope, target_model, leaf_filter, filter_value)
38
+ def join_chain(scope, chain)
39
+ join_hash = build_join_hash_for_chain(chain)
40
+ join_hash.present? ? scope.joins(join_hash) : scope
39
41
  end
40
42
 
41
43
  def build_join_hash_for_chain(chain)
@@ -50,11 +52,14 @@ module JSONAPI
50
52
  end
51
53
  end
52
54
 
53
- def apply_filter_on_model(scope, target_model, filter_name, filter_value)
54
- return scope if empty_filter_value?(filter_value)
55
+ def apply_filter_on_target(scope, target, filter_name, filter_value)
56
+ if target.association
57
+ attributes = { filter_name => filter_value }
58
+ return apply_polymorphic_nested_filters(scope, target.association, target.model, attributes)
59
+ end
55
60
 
56
- apply_column_filter(scope, target_model, filter_name, filter_value) ||
57
- apply_scope_method_filter(scope, target_model, filter_name, filter_value) ||
61
+ apply_column_filter(scope, target.model, filter_name, filter_value) ||
62
+ apply_scope_method_filter(scope, target.model, filter_name, filter_value) ||
58
63
  scope
59
64
  end
60
65
 
@@ -3,8 +3,10 @@
3
3
  module JSONAPI
4
4
  module Support
5
5
  module PolymorphicFilters
6
- def apply_polymorphic_nested_filters(scope, association, _relationship_name, attributes)
7
- context = build_polymorphic_filter_context(association, attributes)
6
+ # `owner` is the model that holds the association. Its table carries the
7
+ # foreign key and type columns, and the scope must already join it.
8
+ def apply_polymorphic_nested_filters(scope, association, owner, attributes)
9
+ context = build_polymorphic_filter_context(association, owner, attributes)
8
10
 
9
11
  attributes.each do |attr_name, attr_value|
10
12
  scope = apply_polymorphic_attribute_filter(scope, attr_name, attr_value, context)
@@ -15,18 +17,18 @@ module JSONAPI
15
17
 
16
18
  private
17
19
 
18
- def build_polymorphic_filter_context(association, attributes)
20
+ def build_polymorphic_filter_context(association, owner, attributes)
19
21
  {
22
+ owner:,
20
23
  foreign_key: association.foreign_key,
21
24
  foreign_type: association.foreign_type,
22
- fk_column: model_class.column_for_attribute(association.foreign_key),
25
+ fk_column: owner.column_for_attribute(association.foreign_key),
23
26
  type_value: attributes["type"] || attributes["type_eq"],
24
27
  class_name: association.options[:class_name],
25
28
  }
26
29
  end
27
30
 
28
31
  def apply_polymorphic_attribute_filter(scope, attr_name, attr_value, context)
29
- return scope if empty_filter_value?(attr_value)
30
32
  return scope if attr_name == "type"
31
33
 
32
34
  scope = apply_polymorphic_id_filter(scope, attr_name, attr_value, context)
@@ -38,7 +40,7 @@ module JSONAPI
38
40
  return apply_polymorphic_fk_filter(scope, attr_value, column_filter, context) if polymorphic_fk_filter?(
39
41
  column_filter, context,
40
42
  )
41
- return scope.where(context[:foreign_key] => attr_value) if attr_name == "id"
43
+ return where_owner_column(scope, context, :foreign_key, attr_value) if attr_name == "id"
42
44
 
43
45
  scope
44
46
  end
@@ -48,17 +50,17 @@ module JSONAPI
48
50
  end
49
51
 
50
52
  def apply_polymorphic_fk_filter(scope, attr_value, column_filter, context)
51
- value = normalize_filter_value_for_model(model_class, context[:fk_column], attr_value)
53
+ value = normalize_filter_value_for_model(context[:owner], context[:fk_column], attr_value)
52
54
  return scope unless value
53
55
 
54
- condition = build_condition(model_class, context[:fk_column], value, column_filter[:operator])
56
+ condition = build_condition(context[:owner], context[:fk_column], value, column_filter[:operator])
55
57
  condition ? apply_condition(scope, condition) : scope
56
58
  end
57
59
 
58
60
  def apply_polymorphic_type_filter(scope, context)
59
61
  return scope unless context[:foreign_type]
60
- return scope.where(context[:foreign_type] => context[:class_name]) if context[:class_name]
61
- return scope.where(context[:foreign_type] => context[:type_value]) if context[:type_value]
62
+ return where_owner_column(scope, context, :foreign_type, context[:class_name]) if context[:class_name]
63
+ return where_owner_column(scope, context, :foreign_type, context[:type_value]) if context[:type_value]
62
64
 
63
65
  scope
64
66
  end
@@ -68,7 +70,13 @@ module JSONAPI
68
70
  return scope if context[:class_name]
69
71
  return scope unless context[:type_value]
70
72
 
71
- scope.where(context[:foreign_type] => context[:type_value])
73
+ where_owner_column(scope, context, :foreign_type, context[:type_value])
74
+ end
75
+
76
+ # Names the owner's table, so the condition reads the joined table when
77
+ # the owner sits below the root of the query.
78
+ def where_owner_column(scope, context, column_key, value)
79
+ scope.where(context[:owner].table_name => { context[column_key] => value })
72
80
  end
73
81
  end
74
82
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JSONAPI
4
- VERSION = "4.1.2"
4
+ VERSION = "4.1.4.pre.202610110318"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jpie
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.1.2
4
+ version: 4.1.4.pre.202610110318
5
5
  platform: ruby
6
6
  authors:
7
7
  - Emil Kampp