jpie 4.1.1 → 4.1.2
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 +4 -4
- data/CHANGELOG.md +28 -0
- data/README.md +4 -0
- data/lib/json_api/controllers/concerns/resource_actions/filter_validation.rb +4 -58
- data/lib/json_api/support/collection_query.rb +8 -1
- data/lib/json_api/support/concerns/nested_filters.rb +13 -49
- data/lib/json_api/support/concerns/regular_filters.rb +1 -0
- data/lib/json_api/support/filter_path.rb +84 -0
- data/lib/json_api/support/sort.rb +21 -4
- data/lib/json_api/version.rb +1 -1
- data/lib/json_api.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7cb5c150e4da0f0608aa5b97534e2239b8b80900054c984ca2aed3c24e747198
|
|
4
|
+
data.tar.gz: 83558900f75be4e9a4b8ea5997f7551963ee1406359b9fd30903d39fe9ad78bb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1fdbf06edc4419def82b3719d3cd419e84bc76f3ebb9af0e64650c8eea9856d0d0a20c688a8c291c491fddaed0f28a417b506391dc63e3987ab8594a34126f4f
|
|
7
|
+
data.tar.gz: cfa30d1e5be610419135bcd16b13752a9eab3792c8082fe95d4a8cc6272103c6ea62c6315bdfc3e46b68b2d7ffe8467b1231e37980e8872b0f3f3781ba71b3d0
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,34 @@ at release time.
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [4.1.2] - 2026-10-10
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **Check each filter in the query, not only in the controller.**
|
|
15
|
+
`CollectionQuery` applied each filter name it received. Only
|
|
16
|
+
`validate_filter_param`, a controller `before_action`, refused a filter no
|
|
17
|
+
resource declares. A caller that built the query without that validation
|
|
18
|
+
reached the public class methods of a model through `public_send`. A spec
|
|
19
|
+
shows `where` take a raw SQL string that way. The query now takes the walk
|
|
20
|
+
filter validation takes, in the new `JSONAPI::Support::FilterPath`. A
|
|
21
|
+
resource declares each hop in `permitted_filters_through`, and the last
|
|
22
|
+
resource declares the leaf in `permitted_filters`. The query ignores a filter
|
|
23
|
+
that fails the walk: it adds no join, no condition and no model call. A
|
|
24
|
+
request that passes filter validation gets the same rows as before. Specs:
|
|
25
|
+
`collection_query_spec.rb`.
|
|
26
|
+
- **End the order of every paged read with the primary key.** A paged
|
|
27
|
+
collection read carried no `ORDER BY` unless the request named a sort, and a
|
|
28
|
+
sort carried no tie-break. A database promises no order beyond the one it is
|
|
29
|
+
given, so a write between two page reads moved a row to another page: the
|
|
30
|
+
walk returned one row twice and never returned another. `Support::Sort` now
|
|
31
|
+
appends the model's primary key to the order of a paged read, and the
|
|
32
|
+
in-memory sort for virtual fields keeps the database order for rows that tie.
|
|
33
|
+
A read with no `page` parameter is unchanged. A paged scope must keep the
|
|
34
|
+
primary key in its select list: PostgreSQL refuses the order on a scope that
|
|
35
|
+
selects `DISTINCT` columns without the key, or groups by another column.
|
|
36
|
+
Regression spec: `paged_read_unstable_order_spec.rb`.
|
|
37
|
+
|
|
10
38
|
## [4.1.1] - 2026-10-10
|
|
11
39
|
|
|
12
40
|
### Fixed
|
data/README.md
CHANGED
|
@@ -256,6 +256,10 @@ Paginated responses include a `links` object with `self`, `first`, `last`, and c
|
|
|
256
256
|
|
|
257
257
|
Default page size is 25, maximum is 100 (configurable).
|
|
258
258
|
|
|
259
|
+
A paged read always has one total order. JPie ends the `ORDER BY` of every paged query with the model's primary key: an unsorted page orders by the key, and a sorted page uses the key for rows that tie on the sort. A client that reads page after page then gets each row once, even when a row changes between two reads. A read with no `page` parameter gets no order of its own.
|
|
260
|
+
|
|
261
|
+
The key cannot help when a row is created or deleted between two page reads. Every later offset then moves by one row.
|
|
262
|
+
|
|
259
263
|
## Includes
|
|
260
264
|
|
|
261
265
|
Include related resources with the `include` parameter:
|
|
@@ -4,74 +4,20 @@ module JSONAPI
|
|
|
4
4
|
module ResourceActions
|
|
5
5
|
module FilterValidation
|
|
6
6
|
extend ActiveSupport::Concern
|
|
7
|
-
include JSONAPI::Support::FilterParsing
|
|
8
7
|
|
|
9
8
|
def validate_filter_param
|
|
10
9
|
filters = parse_filter_param
|
|
11
10
|
return if filters.empty?
|
|
12
11
|
|
|
13
|
-
|
|
12
|
+
path = JSONAPI::Support::FilterPath.new(
|
|
13
|
+
definition: @resource_class, model_class:, namespace: jsonapi_namespace,
|
|
14
|
+
)
|
|
15
|
+
invalid = filters.keys.reject { |name| path.declared?(name) }
|
|
14
16
|
render_filter_errors(invalid) if invalid.any?
|
|
15
17
|
end
|
|
16
18
|
|
|
17
19
|
private
|
|
18
20
|
|
|
19
|
-
def filter_path_valid?(name)
|
|
20
|
-
parts = name.split(".")
|
|
21
|
-
check_filter_parts(parts, @resource_class, model_class)
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def check_filter_parts(parts, res_cls, mod_cls)
|
|
25
|
-
return false if parts.empty?
|
|
26
|
-
return single_filter_valid?(parts.first, res_cls) if parts.length == 1
|
|
27
|
-
|
|
28
|
-
check_nested_filter(parts, res_cls, mod_cls)
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def single_filter_valid?(name, res_cls)
|
|
32
|
-
res_cls.permitted_filters.map(&:to_s).include?(name)
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def check_nested_filter(parts, res_cls, mod_cls)
|
|
36
|
-
rel, *rest = parts
|
|
37
|
-
return false unless filter_rel_allowed?(res_cls, rel)
|
|
38
|
-
|
|
39
|
-
assoc = mod_cls.reflect_on_association(rel.to_sym)
|
|
40
|
-
return false unless assoc
|
|
41
|
-
return polymorphic_filter_valid?(rest) if assoc.polymorphic?
|
|
42
|
-
|
|
43
|
-
check_related_filter(rest, assoc)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def check_related_filter(parts, assoc)
|
|
47
|
-
rel_mod = assoc.klass
|
|
48
|
-
rel_res = filter_hop_resource(rel_mod)
|
|
49
|
-
return false unless rel_res
|
|
50
|
-
|
|
51
|
-
check_filter_parts(parts, rel_res, rel_mod)
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
# The request namespace answers each hop, the lookup an include hop
|
|
55
|
-
# takes. A flat resource is another namespace's surface, with filters
|
|
56
|
-
# this one never declared.
|
|
57
|
-
def filter_hop_resource(model)
|
|
58
|
-
JSONAPI::ResourceLoader.find_for_model(model, namespace: jsonapi_namespace)
|
|
59
|
-
rescue JSONAPI::ResourceLoader::MissingResourceClass
|
|
60
|
-
nil
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def filter_rel_allowed?(res_cls, rel)
|
|
64
|
-
permitted = res_cls.permitted_filters_through
|
|
65
|
-
permitted.include?(rel.to_sym) || permitted.map(&:to_s).include?(rel.to_s)
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def polymorphic_filter_valid?(parts)
|
|
69
|
-
return false if parts.empty? || parts.length > 1
|
|
70
|
-
|
|
71
|
-
col_filter = parse_column_filter(parts.first)
|
|
72
|
-
%w[id type].include?(col_filter ? col_filter[:column] : parts.first)
|
|
73
|
-
end
|
|
74
|
-
|
|
75
21
|
def render_filter_errors(invalid)
|
|
76
22
|
render_parameter_errors(
|
|
77
23
|
invalid,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "sort"
|
|
4
|
+
require_relative "filter_path"
|
|
4
5
|
require_relative "concerns/regular_filters"
|
|
5
6
|
require_relative "concerns/nested_filters"
|
|
6
7
|
require_relative "concerns/polymorphic_filters"
|
|
@@ -30,7 +31,7 @@ module JSONAPI
|
|
|
30
31
|
|
|
31
32
|
def execute
|
|
32
33
|
@scope = apply_filtering
|
|
33
|
-
@sort = Support::Sort.new(sort_params:, resource_class: definition)
|
|
34
|
+
@sort = Support::Sort.new(sort_params:, resource_class: definition, tie_break: pagination_applied)
|
|
34
35
|
compute_total_count_if_needed
|
|
35
36
|
@scope = @sort.apply(@scope)
|
|
36
37
|
@scope = apply_pagination
|
|
@@ -51,5 +52,11 @@ module JSONAPI
|
|
|
51
52
|
scope = apply_nested_relationship_filters(@scope)
|
|
52
53
|
apply_regular_filters(scope)
|
|
53
54
|
end
|
|
55
|
+
|
|
56
|
+
# The query trusts no caller to validate first. It applies a filter only
|
|
57
|
+
# when the resources on its path declare it.
|
|
58
|
+
def filter_path
|
|
59
|
+
@filter_path ||= Support::FilterPath.new(definition:, model_class:, namespace:)
|
|
60
|
+
end
|
|
54
61
|
end
|
|
55
62
|
end
|
|
@@ -17,61 +17,25 @@ module JSONAPI
|
|
|
17
17
|
private
|
|
18
18
|
|
|
19
19
|
def apply_filter_for_path(scope, filter_name, filter_value)
|
|
20
|
-
|
|
21
|
-
return scope if
|
|
20
|
+
*relationship_chain, leaf_filter = filter_name.split(".")
|
|
21
|
+
return scope if relationship_chain.empty?
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
# The walk filter validation takes. A path it refuses filters nothing.
|
|
24
|
+
target = filter_path.resolve(filter_name)
|
|
25
|
+
return scope unless target
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
return result[:scope] if result[:early_return]
|
|
28
|
-
|
|
29
|
-
apply_joined_filter(scope, result, relationship_chain:, leaf_filter:, filter_value:)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def traverse_relationship_chain(scope, relationship_chain, leaf_filter, filter_value)
|
|
33
|
-
current_model = model_class
|
|
34
|
-
current_definition = definition
|
|
35
|
-
|
|
36
|
-
relationship_chain.each do |relationship_name|
|
|
37
|
-
result = process_chain_step(scope, current_model, relationship_name, leaf_filter, filter_value)
|
|
38
|
-
return result if result[:early_return]
|
|
39
|
-
|
|
40
|
-
current_model = result[:model]
|
|
41
|
-
current_definition = result[:definition]
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
{ model: current_model, definition: current_definition, early_return: false }
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def process_chain_step(scope, current_model, relationship_name, leaf_filter, filter_value)
|
|
48
|
-
association = current_model.reflect_on_association(relationship_name.to_sym)
|
|
49
|
-
return { scope:, early_return: true } unless association
|
|
50
|
-
|
|
51
|
-
if association.polymorphic?
|
|
27
|
+
if target.association
|
|
52
28
|
attributes = { leaf_filter => filter_value }
|
|
53
|
-
return
|
|
54
|
-
early_return: true, }
|
|
29
|
+
return apply_polymorphic_nested_filters(scope, target.association, relationship_chain.last, attributes)
|
|
55
30
|
end
|
|
56
31
|
|
|
57
|
-
|
|
58
|
-
return { scope:, early_return: true } unless next_definition
|
|
59
|
-
|
|
60
|
-
{ model: association.klass, definition: next_definition, early_return: false }
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
# The query's namespace answers each hop, as it does in filter
|
|
64
|
-
# validation. A hop it carries no resource for filters nothing.
|
|
65
|
-
def hop_definition(model)
|
|
66
|
-
JSONAPI::ResourceLoader.find_for_model(model, namespace:)
|
|
67
|
-
rescue JSONAPI::ResourceLoader::MissingResourceClass
|
|
68
|
-
nil
|
|
32
|
+
apply_joined_filter(scope, target.model, relationship_chain:, leaf_filter:, filter_value:)
|
|
69
33
|
end
|
|
70
34
|
|
|
71
|
-
def apply_joined_filter(scope,
|
|
35
|
+
def apply_joined_filter(scope, target_model, relationship_chain:, leaf_filter:, filter_value:)
|
|
72
36
|
join_hash = build_join_hash_for_chain(relationship_chain)
|
|
73
37
|
scope = scope.joins(join_hash) if join_hash.present?
|
|
74
|
-
apply_filter_on_model(scope,
|
|
38
|
+
apply_filter_on_model(scope, target_model, leaf_filter, filter_value)
|
|
75
39
|
end
|
|
76
40
|
|
|
77
41
|
def build_join_hash_for_chain(chain)
|
|
@@ -86,15 +50,15 @@ module JSONAPI
|
|
|
86
50
|
end
|
|
87
51
|
end
|
|
88
52
|
|
|
89
|
-
def apply_filter_on_model(scope, target_model,
|
|
53
|
+
def apply_filter_on_model(scope, target_model, filter_name, filter_value)
|
|
90
54
|
return scope if empty_filter_value?(filter_value)
|
|
91
55
|
|
|
92
56
|
apply_column_filter(scope, target_model, filter_name, filter_value) ||
|
|
93
|
-
apply_scope_method_filter(scope, target_model,
|
|
57
|
+
apply_scope_method_filter(scope, target_model, filter_name, filter_value) ||
|
|
94
58
|
scope
|
|
95
59
|
end
|
|
96
60
|
|
|
97
|
-
def apply_scope_method_filter(scope, target_model,
|
|
61
|
+
def apply_scope_method_filter(scope, target_model, filter_name, filter_value)
|
|
98
62
|
return nil unless target_model.respond_to?(filter_name.to_sym)
|
|
99
63
|
|
|
100
64
|
try_scope_method(scope, target_model, filter_name, filter_value)
|
|
@@ -18,6 +18,7 @@ module JSONAPI
|
|
|
18
18
|
|
|
19
19
|
def apply_regular_filter(scope, filter_name, filter_value)
|
|
20
20
|
return scope if empty_filter_value?(filter_value)
|
|
21
|
+
return scope unless filter_path.declared?(filter_name)
|
|
21
22
|
|
|
22
23
|
apply_column_filter(scope, model_class, filter_name, filter_value) ||
|
|
23
24
|
apply_scope_fallback(scope, filter_name, filter_value)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "filter_parsing"
|
|
4
|
+
|
|
5
|
+
module JSONAPI
|
|
6
|
+
module Support
|
|
7
|
+
# Walks a filter path such as `comments.author.email_eq`, for filter
|
|
8
|
+
# validation and for the query. A resource declares each hop as a
|
|
9
|
+
# relationship, and the last resource declares the leaf as a filter. A path
|
|
10
|
+
# that fails either test resolves to nothing.
|
|
11
|
+
class FilterPath
|
|
12
|
+
include FilterParsing
|
|
13
|
+
|
|
14
|
+
# Where a declared path ends. `association` is the polymorphic hop that
|
|
15
|
+
# ends the walk, and nil on every other path.
|
|
16
|
+
Target = Struct.new(:model, :definition, :association)
|
|
17
|
+
|
|
18
|
+
def initialize(definition:, model_class:, namespace:)
|
|
19
|
+
@definition = definition
|
|
20
|
+
@model_class = model_class
|
|
21
|
+
@namespace = namespace
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def declared?(name)
|
|
25
|
+
!resolve(name).nil?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def resolve(name)
|
|
29
|
+
*relationships, filter = name.to_s.split(".")
|
|
30
|
+
return nil unless filter
|
|
31
|
+
|
|
32
|
+
target = walk(relationships)
|
|
33
|
+
target if target && filter_declared?(target, filter)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def walk(relationships)
|
|
39
|
+
target = Target.new(@model_class, @definition)
|
|
40
|
+
relationships.each do |relationship|
|
|
41
|
+
# A polymorphic hop names no one resource, so no hop follows it.
|
|
42
|
+
target = target.association ? nil : hop(target, relationship)
|
|
43
|
+
return nil unless target
|
|
44
|
+
end
|
|
45
|
+
target
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def hop(target, relationship)
|
|
49
|
+
return nil unless listed?(target.definition.permitted_filters_through, relationship)
|
|
50
|
+
|
|
51
|
+
association = target.model.reflect_on_association(relationship.to_sym)
|
|
52
|
+
return nil unless association
|
|
53
|
+
return Target.new(target.model, target.definition, association) if association.polymorphic?
|
|
54
|
+
|
|
55
|
+
definition = hop_definition(association.klass)
|
|
56
|
+
definition && Target.new(association.klass, definition)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# The namespace answers each hop, the lookup an include hop takes. A flat
|
|
60
|
+
# resource is another namespace's surface, with filters this one never
|
|
61
|
+
# declared.
|
|
62
|
+
def hop_definition(model)
|
|
63
|
+
JSONAPI::ResourceLoader.find_for_model(model, namespace: @namespace)
|
|
64
|
+
rescue JSONAPI::ResourceLoader::MissingResourceClass
|
|
65
|
+
nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def filter_declared?(target, filter)
|
|
69
|
+
return polymorphic_filter?(filter) if target.association
|
|
70
|
+
|
|
71
|
+
listed?(target.definition.permitted_filters, filter)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def polymorphic_filter?(filter)
|
|
75
|
+
column_filter = parse_column_filter(filter)
|
|
76
|
+
%w[id type].include?(column_filter ? column_filter[:column] : filter)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def listed?(names, name)
|
|
80
|
+
names.map(&:to_s).include?(name)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -17,6 +17,10 @@ module JSONAPI
|
|
|
17
17
|
# sort over the materialised result set. This means virtual sorts always
|
|
18
18
|
# take precedence over column/scope sorts regardless of the order the
|
|
19
19
|
# user specified them in.
|
|
20
|
+
#
|
|
21
|
+
# With tie_break, the order ends with the primary key. A paged read needs
|
|
22
|
+
# that: its pages are separate queries, and only a total order makes them
|
|
23
|
+
# agree on which row comes where.
|
|
20
24
|
class Sort
|
|
21
25
|
class InvalidFieldError < JSONAPI::Error
|
|
22
26
|
attr_reader :invalid_fields
|
|
@@ -27,15 +31,15 @@ module JSONAPI
|
|
|
27
31
|
end
|
|
28
32
|
end
|
|
29
33
|
|
|
30
|
-
def initialize(sort_params:, resource_class:)
|
|
34
|
+
def initialize(sort_params:, resource_class:, tie_break: false)
|
|
31
35
|
@fields = build_fields(sort_params, resource_class)
|
|
36
|
+
@tie_break = tie_break
|
|
32
37
|
end
|
|
33
38
|
|
|
34
39
|
def apply(scope)
|
|
35
|
-
return scope if @fields.empty?
|
|
36
|
-
|
|
37
40
|
virtual, db = @fields.partition(&:virtual?)
|
|
38
41
|
scope = db.reduce(scope) { |acc, field| field.apply(acc) }
|
|
42
|
+
scope = break_ties(scope) if @tie_break
|
|
39
43
|
virtual.any? ? apply_virtual_sorting(scope, virtual) : scope
|
|
40
44
|
end
|
|
41
45
|
|
|
@@ -64,8 +68,21 @@ module JSONAPI
|
|
|
64
68
|
raise InvalidFieldError, invalid if invalid.any?
|
|
65
69
|
end
|
|
66
70
|
|
|
71
|
+
# A database places tied rows as it finds them, and an update moves a row.
|
|
72
|
+
# The primary key gives every row one place, so two queries agree on the order.
|
|
73
|
+
def break_ties(scope)
|
|
74
|
+
return scope unless scope.is_a?(ActiveRecord::Relation)
|
|
75
|
+
|
|
76
|
+
scope.order(Array(scope.primary_key).index_with(:asc))
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Array#sort does not keep tied items in place, so the position the scope gave breaks the tie.
|
|
67
80
|
def apply_virtual_sorting(scope, virtual_fields)
|
|
68
|
-
scope.to_a.sort
|
|
81
|
+
ranked = scope.to_a.each_with_index.sort do |(record_a, position_a), (record_b, position_b)|
|
|
82
|
+
comparison = compare_by_fields(record_a, record_b, virtual_fields)
|
|
83
|
+
comparison.zero? ? position_a <=> position_b : comparison
|
|
84
|
+
end
|
|
85
|
+
ranked.map(&:first)
|
|
69
86
|
end
|
|
70
87
|
|
|
71
88
|
def compare_by_fields(record_a, record_b, fields)
|
data/lib/json_api/version.rb
CHANGED
data/lib/json_api.rb
CHANGED
|
@@ -37,6 +37,7 @@ require "json_api/active_storage/serialization"
|
|
|
37
37
|
require "json_api/active_storage/deserialization"
|
|
38
38
|
require "json_api/support/active_storage_support"
|
|
39
39
|
require "json_api/support/filter_parsing"
|
|
40
|
+
require "json_api/support/filter_path"
|
|
40
41
|
require "json_api/support/collection_query"
|
|
41
42
|
require "json_api/support/prosopite_instrumentation_logger"
|
|
42
43
|
require "json_api/support/n1_log_subscriber"
|
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.
|
|
4
|
+
version: 4.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Emil Kampp
|
|
@@ -166,6 +166,7 @@ files:
|
|
|
166
166
|
- lib/json_api/support/concerns/regular_filters.rb
|
|
167
167
|
- lib/json_api/support/correlation_id.rb
|
|
168
168
|
- lib/json_api/support/filter_parsing.rb
|
|
169
|
+
- lib/json_api/support/filter_path.rb
|
|
169
170
|
- lib/json_api/support/header_warning_subscriber.rb
|
|
170
171
|
- lib/json_api/support/instrumentation.rb
|
|
171
172
|
- lib/json_api/support/n1_log_subscriber.rb
|