praxis 2.0.pre.10 → 2.0.pre.15
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/.travis.yml +1 -3
- data/CHANGELOG.md +26 -0
- data/bin/praxis +65 -2
- data/lib/praxis/api_definition.rb +8 -4
- data/lib/praxis/bootloader_stages/environment.rb +1 -0
- data/lib/praxis/collection.rb +11 -0
- data/lib/praxis/docs/open_api/response_object.rb +21 -6
- data/lib/praxis/docs/open_api_generator.rb +1 -1
- data/lib/praxis/extensions/attribute_filtering.rb +14 -1
- data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +206 -66
- data/lib/praxis/extensions/attribute_filtering/filter_tree_node.rb +3 -2
- data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +45 -41
- data/lib/praxis/extensions/attribute_filtering/filters_parser.rb +193 -0
- data/lib/praxis/extensions/attribute_filtering/sequel_filter_query_builder.rb +20 -8
- data/lib/praxis/extensions/pagination.rb +5 -32
- data/lib/praxis/mapper/active_model_compat.rb +4 -0
- data/lib/praxis/mapper/resource.rb +18 -2
- data/lib/praxis/mapper/selector_generator.rb +1 -0
- data/lib/praxis/mapper/sequel_compat.rb +7 -0
- data/lib/praxis/media_type_identifier.rb +11 -1
- data/lib/praxis/plugins/mapper_plugin.rb +22 -13
- data/lib/praxis/plugins/pagination_plugin.rb +34 -4
- data/lib/praxis/response_definition.rb +46 -66
- data/lib/praxis/responses/http.rb +3 -1
- data/lib/praxis/tasks/api_docs.rb +4 -1
- data/lib/praxis/tasks/routes.rb +6 -6
- data/lib/praxis/version.rb +1 -1
- data/spec/praxis/action_definition_spec.rb +3 -1
- data/spec/praxis/extensions/attribute_filtering/active_record_filter_query_builder_spec.rb +267 -167
- data/spec/praxis/extensions/attribute_filtering/filter_tree_node_spec.rb +25 -6
- data/spec/praxis/extensions/attribute_filtering/filtering_params_spec.rb +100 -17
- data/spec/praxis/extensions/attribute_filtering/filters_parser_spec.rb +148 -0
- data/spec/praxis/extensions/field_selection/active_record_query_selector_spec.rb +1 -1
- data/spec/praxis/extensions/field_selection/sequel_query_selector_spec.rb +1 -1
- data/spec/praxis/extensions/support/spec_resources_active_model.rb +1 -1
- data/spec/praxis/mapper/selector_generator_spec.rb +1 -1
- data/spec/praxis/media_type_identifier_spec.rb +15 -1
- data/spec/praxis/response_definition_spec.rb +37 -129
- data/tasks/thor/example.rb +12 -6
- data/tasks/thor/model.rb +40 -0
- data/tasks/thor/scaffold.rb +117 -0
- data/tasks/thor/templates/generator/empty_app/config/environment.rb +1 -0
- data/tasks/thor/templates/generator/example_app/Rakefile +9 -2
- data/tasks/thor/templates/generator/example_app/app/v1/concerns/controller_base.rb +24 -0
- data/tasks/thor/templates/generator/example_app/app/v1/concerns/href.rb +33 -0
- data/tasks/thor/templates/generator/example_app/app/v1/controllers/users.rb +2 -2
- data/tasks/thor/templates/generator/example_app/app/v1/resources/base.rb +15 -0
- data/tasks/thor/templates/generator/example_app/app/v1/resources/user.rb +7 -28
- data/tasks/thor/templates/generator/example_app/config.ru +1 -2
- data/tasks/thor/templates/generator/example_app/config/environment.rb +3 -2
- data/tasks/thor/templates/generator/example_app/db/migrate/20201010101010_create_users_table.rb +3 -2
- data/tasks/thor/templates/generator/example_app/db/seeds.rb +6 -0
- data/tasks/thor/templates/generator/example_app/design/v1/endpoints/users.rb +4 -4
- data/tasks/thor/templates/generator/example_app/design/v1/media_types/user.rb +1 -6
- data/tasks/thor/templates/generator/example_app/spec/helpers/database_helper.rb +4 -2
- data/tasks/thor/templates/generator/example_app/spec/spec_helper.rb +2 -2
- data/tasks/thor/templates/generator/example_app/spec/v1/controllers/users_spec.rb +2 -2
- data/tasks/thor/templates/generator/scaffold/design/endpoints/collection.rb +98 -0
- data/tasks/thor/templates/generator/scaffold/design/media_types/item.rb +18 -0
- data/tasks/thor/templates/generator/scaffold/implementation/controllers/collection.rb +77 -0
- data/tasks/thor/templates/generator/scaffold/implementation/resources/base.rb +11 -0
- data/tasks/thor/templates/generator/scaffold/implementation/resources/item.rb +45 -0
- data/tasks/thor/templates/generator/scaffold/models/active_record.rb +6 -0
- data/tasks/thor/templates/generator/scaffold/models/sequel.rb +6 -0
- metadata +21 -6
@@ -3,7 +3,8 @@ module Praxis
|
|
3
3
|
module AttributeFiltering
|
4
4
|
class FilterTreeNode
|
5
5
|
attr_reader :path, :conditions, :children
|
6
|
-
#
|
6
|
+
# Parsed_filters is an Array of {name: X, op: Y, value: Z} ... exactly the format of the FilteringParams.load method
|
7
|
+
# It can also contain a :node_object
|
7
8
|
def initialize(parsed_filters, path: [])
|
8
9
|
@path = path # Array that marks the tree 'path' to this node (with respect to the absolute root)
|
9
10
|
@conditions = [] # Conditions to apply directly to this node
|
@@ -14,7 +15,7 @@ module Praxis
|
|
14
15
|
if components.empty?
|
15
16
|
return
|
16
17
|
elsif components.size == 1
|
17
|
-
@conditions << hash.slice(:name, :op, :value)
|
18
|
+
@conditions << hash.slice(:name, :op, :value, :fuzzy, :node_object)
|
18
19
|
else
|
19
20
|
children_data[components.first] ||= []
|
20
21
|
children_data[components.first] << hash
|
@@ -1,7 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
|
2
|
+
require 'praxis/extensions/attribute_filtering/filters_parser'
|
3
|
+
|
3
4
|
#
|
4
|
-
# Attributor type to define and
|
5
|
+
# Attributor type to define and handle the language to express filtering attributes in listings.
|
5
6
|
# Commonly used in a query string parameter value for listing calls.
|
6
7
|
#
|
7
8
|
# The type allows you to restrict the allowable fields (and their types) based on an existing Mediatype.
|
@@ -14,7 +15,7 @@
|
|
14
15
|
# attribute :filters,
|
15
16
|
# Types::FilteringParams.for(MediaTypes::MyType) do
|
16
17
|
# filter 'user.id', using: ['=', '!=']
|
17
|
-
# filter 'name', using: ['=', '!=']
|
18
|
+
# filter 'name', using: ['=', '!=', '!', '!!]
|
18
19
|
# filter 'children.created_at', using: ['>', '>=', '<', '<=']
|
19
20
|
# filter 'display_name', using: ['=', '!='], fuzzy: true
|
20
21
|
# end
|
@@ -26,6 +27,8 @@ module Praxis
|
|
26
27
|
include Attributor::Type
|
27
28
|
include Attributor::Dumpable
|
28
29
|
|
30
|
+
attr_reader :parsed_array
|
31
|
+
|
29
32
|
class DSLCompiler < Attributor::DSLCompiler
|
30
33
|
# "account.id": { operators: ["=", "!="] },
|
31
34
|
# name: { operators: ["=", "!="], fuzzy_match: true },
|
@@ -36,9 +39,9 @@ module Praxis
|
|
36
39
|
end
|
37
40
|
end
|
38
41
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
+
VALUE_OPERATORS = Set.new(['!=', '>=', '<=', '=', '<', '>']).freeze
|
43
|
+
NOVALUE_OPERATORS = Set.new(['!','!!']).freeze
|
44
|
+
AVAILABLE_OPERATORS = Set.new(VALUE_OPERATORS+NOVALUE_OPERATORS).freeze
|
42
45
|
|
43
46
|
# Abstract class, which needs to be used by subclassing it through the .for method, to set the allowed filters
|
44
47
|
# definition should be a hash, keyed by field name, which contains a hash that can have two pieces of metadata
|
@@ -52,7 +55,7 @@ module Praxis
|
|
52
55
|
def for(media_type, **_opts)
|
53
56
|
unless media_type < Praxis::MediaType
|
54
57
|
raise ArgumentError, "Invalid type: #{media_type.name} for Filters. " \
|
55
|
-
'
|
58
|
+
'Using the .for method for defining a filter, requires passing a subclass of a MediaType'
|
56
59
|
end
|
57
60
|
|
58
61
|
::Class.new(self) do
|
@@ -77,9 +80,7 @@ module Praxis
|
|
77
80
|
}
|
78
81
|
end
|
79
82
|
end
|
80
|
-
|
81
|
-
attr_reader :parsed_array
|
82
|
-
|
83
|
+
|
83
84
|
def self.native_type
|
84
85
|
self
|
85
86
|
end
|
@@ -134,11 +135,15 @@ module Praxis
|
|
134
135
|
raise "filter with name #{filter_name} does not correspond to an existing field inside " \
|
135
136
|
" MediaType #{media_type.name}"
|
136
137
|
end
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
138
|
+
if NOVALUE_OPERATORS.include?(op)
|
139
|
+
arr << "#{filter_name}#{op}" # Do not add a value for the operators that don't take it
|
140
|
+
else
|
141
|
+
attr_example = filter_components.inject(mt_example) do |last, name|
|
142
|
+
# we can safely do sends, since we've verified the components are valid
|
143
|
+
last.send(name)
|
144
|
+
end
|
145
|
+
arr << "#{filter_name}#{op}#{attr_example}"
|
146
|
+
end
|
142
147
|
end.join('&')
|
143
148
|
else
|
144
149
|
'name=Joe&date>2017-01-01'
|
@@ -153,34 +158,33 @@ module Praxis
|
|
153
158
|
|
154
159
|
def self.load(filters, _context = Attributor::DEFAULT_ROOT_CONTEXT, **_options)
|
155
160
|
return filters if filters.is_a?(native_type)
|
156
|
-
return new if filters.nil?
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
161
|
+
return new if filters.nil? || filters.blank?
|
162
|
+
|
163
|
+
parsed = Parser.new.parse(filters)
|
164
|
+
|
165
|
+
tree = ConditionGroup.load(parsed)
|
166
|
+
|
167
|
+
rr = tree.flattened_conditions
|
168
|
+
accum = []
|
169
|
+
rr.each do |spec|
|
170
|
+
attr_name = spec[:name]
|
171
|
+
# TODO: Do we need to CGI.unescape things? here or even before??...
|
172
|
+
coerced = \
|
173
|
+
if media_type
|
174
|
+
filter_components = attr_name.to_s.split('.').map(&:to_sym)
|
175
|
+
attr, _enclosing_type = find_filter_attribute(filter_components, media_type)
|
176
|
+
if spec[:values].is_a?(Array)
|
177
|
+
attr_coll = Attributor::Collection.of(attr.type)
|
178
|
+
attr_coll.load(spec[:values])
|
179
|
+
else
|
180
|
+
attr.load(spec[:values])
|
181
|
+
end
|
175
182
|
else
|
176
|
-
|
183
|
+
spec[:values]
|
177
184
|
end
|
178
|
-
|
179
|
-
value
|
180
|
-
end
|
181
|
-
arr.push(name: attr_name, op: match[:operator], value: coerced )
|
185
|
+
accum.push(name: attr_name, op: spec[:op], value: coerced , fuzzy: spec[:fuzzies], node_object: spec[:node_object])
|
182
186
|
end
|
183
|
-
new(
|
187
|
+
new(accum)
|
184
188
|
end
|
185
189
|
|
186
190
|
def self.dump(value, **_opts)
|
@@ -221,7 +225,7 @@ module Praxis
|
|
221
225
|
value = item[:value]
|
222
226
|
unless value.empty?
|
223
227
|
fuzzy_match = attr_filters[:fuzzy_match]
|
224
|
-
if
|
228
|
+
if item[:fuzzy] && !item[:fuzzy].empty? && !fuzzy_match
|
225
229
|
errors << "Fuzzy matching for #{attr_name} is not allowed (yet '*' was found in the value)"
|
226
230
|
end
|
227
231
|
end
|
@@ -0,0 +1,193 @@
|
|
1
|
+
require 'parslet'
|
2
|
+
|
3
|
+
module Praxis
|
4
|
+
module Extensions
|
5
|
+
module AttributeFiltering
|
6
|
+
class FilteringParams
|
7
|
+
class Condition
|
8
|
+
attr_reader :name, :op, :values
|
9
|
+
attr_accessor :parent_group
|
10
|
+
|
11
|
+
# For operands with a single or no values: Incoming data is a hash with name and op
|
12
|
+
# For Operands with multiple values: Incoming data is an array of hashes
|
13
|
+
# First hash has the spec (i.e., name and op)
|
14
|
+
# The rest of the hashes contain a value each (a hash with value: X each).
|
15
|
+
# Example: [{:name=>"multi"@0, :op=>"="@5}, {:value=>"1"@6}, {:value=>"2"@8}]
|
16
|
+
def initialize(triad:, parent_group:)
|
17
|
+
@parent_group = parent_group
|
18
|
+
if triad.is_a? Array # several values coming in
|
19
|
+
spec, *values = triad
|
20
|
+
@name = spec[:name].to_sym
|
21
|
+
@op = spec[:op].to_s
|
22
|
+
|
23
|
+
if values.empty?
|
24
|
+
@values = ""
|
25
|
+
@fuzzies = nil
|
26
|
+
elsif values.size == 1
|
27
|
+
raw_val = values.first[:value].to_s
|
28
|
+
@values, @fuzzies = _compute_fuzzy(values.first[:value].to_s)
|
29
|
+
else
|
30
|
+
@values = []
|
31
|
+
@fuzzies = []
|
32
|
+
results = values.each do|e|
|
33
|
+
val, fuz = _compute_fuzzy(e[:value].to_s)
|
34
|
+
@values.push val
|
35
|
+
@fuzzies.push fuz
|
36
|
+
end
|
37
|
+
end
|
38
|
+
else # No values for the operand
|
39
|
+
@name = triad[:name].to_sym
|
40
|
+
@op = triad[:op].to_s
|
41
|
+
if ['!','!!'].include?(@op)
|
42
|
+
@values, @fuzzies = [nil, nil]
|
43
|
+
else
|
44
|
+
# Value operand without value? => convert it to empty string
|
45
|
+
raise "Interesting, didn't know this could happen. Oops!" if triad[:value].is_a?(Array) && !triad[:value].empty?
|
46
|
+
if triad[:value] == []
|
47
|
+
@values, @fuzzies = ['', nil]
|
48
|
+
else
|
49
|
+
@values, @fuzzies = _compute_fuzzy(triad[:value].to_s)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
# Takes a raw val, and spits out the output val (unescaped), and the fuzzy definition
|
55
|
+
def _compute_fuzzy(raw_val)
|
56
|
+
starting = raw_val[0] == '*'
|
57
|
+
ending = raw_val[-1] == '*'
|
58
|
+
newval, fuzzy = if starting && ending
|
59
|
+
[raw_val[1..-2], :start_end]
|
60
|
+
elsif starting
|
61
|
+
[raw_val[1..-1], :start]
|
62
|
+
elsif ending
|
63
|
+
[raw_val[0..-2], :end]
|
64
|
+
else
|
65
|
+
[raw_val,nil]
|
66
|
+
end
|
67
|
+
newval = CGI.unescape(newval) if newval
|
68
|
+
[newval,fuzzy]
|
69
|
+
end
|
70
|
+
def flattened_conditions
|
71
|
+
[{name: @name, op: @op, values: @values, fuzzies: @fuzzies, node_object: self}]
|
72
|
+
end
|
73
|
+
|
74
|
+
# Dumps the value, marking where the fuzzy might be, and removing the * to differentiate from literals
|
75
|
+
def _dump_value(val,fuzzy)
|
76
|
+
case fuzzy
|
77
|
+
when nil
|
78
|
+
val
|
79
|
+
when :start_end
|
80
|
+
'{*}' + val + '{*}'
|
81
|
+
when :start
|
82
|
+
'{*}' + val
|
83
|
+
when :end
|
84
|
+
val +'{*}'
|
85
|
+
end
|
86
|
+
end
|
87
|
+
def dump
|
88
|
+
vals = if values.is_a? Array
|
89
|
+
dumped = values.map.with_index{|val,i| _dump_value(val, @fuzzies[i])}
|
90
|
+
"[#{dumped.join(',')}]" # Purposedly enclose in brackets to make sure we differentiate
|
91
|
+
else
|
92
|
+
(values == '') ? '""' : _dump_value(values,@fuzzies) # Dump the empty string explicitly with quotes if we've converted no value to empty string
|
93
|
+
end
|
94
|
+
"#{name}#{op}#{vals}"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# An Object that represents an AST tree for either an OR or an AND conditions
|
99
|
+
# to be applied to its items children
|
100
|
+
class ConditionGroup
|
101
|
+
attr_reader :items, :type
|
102
|
+
attr_accessor :parent_group
|
103
|
+
attr_accessor :associated_query # Metadata to be used by whomever is manipulating this
|
104
|
+
|
105
|
+
def self.load(node)
|
106
|
+
unless node[:o]
|
107
|
+
loaded = Condition.new(triad: node[:triad], parent_group: nil)
|
108
|
+
else
|
109
|
+
compactedl = compress_tree(node: node[:l], op: node[:o])
|
110
|
+
compactedr = compress_tree(node: node[:r], op: node[:o])
|
111
|
+
compacted = {op: node[:o], items: compactedl + compactedr }
|
112
|
+
|
113
|
+
loaded = ConditionGroup.new(**compacted, parent_group: nil)
|
114
|
+
end
|
115
|
+
loaded
|
116
|
+
end
|
117
|
+
|
118
|
+
def initialize(op:, items:, parent_group:)
|
119
|
+
@type = (op.to_s == '&') ? :and : :or
|
120
|
+
@items = items.map do |item|
|
121
|
+
if item[:op]
|
122
|
+
ConditionGroup.new(**item, parent_group: self)
|
123
|
+
else
|
124
|
+
Condition.new(triad: item[:triad], parent_group: self)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
@parent_group = parent_group
|
128
|
+
end
|
129
|
+
|
130
|
+
def dump
|
131
|
+
"( " + @items.map(&:dump).join(" #{type.upcase} ") + " )"
|
132
|
+
end
|
133
|
+
|
134
|
+
# Returns an array with flat conditions from all child triad conditions
|
135
|
+
def flattened_conditions
|
136
|
+
@items.inject([]) do |accum, item|
|
137
|
+
accum + item.flattened_conditions
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# Given a binary tree of operand conditions, transform it to a multi-leaf tree
|
142
|
+
# where a single condition node has potentially multiple subtrees for the same operation (instead of 2)
|
143
|
+
# For example (&, (&, a, b), (|, c, d)) => (&, a, b, (|, c, d))
|
144
|
+
def self.compress_tree(node:, op:)
|
145
|
+
if node[:triad]
|
146
|
+
return [node]
|
147
|
+
end
|
148
|
+
|
149
|
+
# It is an op node
|
150
|
+
if node[:o] == op
|
151
|
+
# compatible op as parent, collect my compacted children and return them up skipping my op
|
152
|
+
resultl = compress_tree(node: node[:l], op: op)
|
153
|
+
resultr = compress_tree(node: node[:r], op: op)
|
154
|
+
resultl+resultr
|
155
|
+
else
|
156
|
+
collected = compress_tree(node: node, op: node[:o])
|
157
|
+
[{op: node[:o], items: collected }]
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
class Parser < Parslet::Parser
|
163
|
+
root :expression
|
164
|
+
rule(:lparen) { str('(') }
|
165
|
+
rule(:rparen) { str(')') }
|
166
|
+
rule(:comma) { str(',') }
|
167
|
+
rule(:val_operator) { str('!=') | str('>=') | str('<=') | str('=') | str('<') | str('>')}
|
168
|
+
rule(:noval_operator) { str('!!') | str('!')}
|
169
|
+
rule(:and_kw) { str('&') }
|
170
|
+
rule(:or_kw) { str('|') }
|
171
|
+
|
172
|
+
def infix *args
|
173
|
+
Infix.new(*args)
|
174
|
+
end
|
175
|
+
|
176
|
+
rule(:name) { match('[a-zA-Z0-9_\.]').repeat(1) } # TODO: are these the only characters that we allow for names?
|
177
|
+
rule(:chars) { match('[^&|(),]').repeat(0).as(:value) }
|
178
|
+
rule(:value) { chars >> (comma >> chars ).repeat }
|
179
|
+
|
180
|
+
rule(:triad) {
|
181
|
+
(name.as(:name) >> val_operator.as(:op) >> value).as(:triad) |
|
182
|
+
(name.as(:name) >> noval_operator.as(:op)).as(:triad) |
|
183
|
+
lparen >> expression >> rparen
|
184
|
+
}
|
185
|
+
|
186
|
+
rule(:expression) {
|
187
|
+
infix_expression(triad, [and_kw, 2, :left], [or_kw, 1, :right])
|
188
|
+
}
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
@@ -9,7 +9,7 @@ module Praxis
|
|
9
9
|
class << self
|
10
10
|
def for(definition)
|
11
11
|
Class.new(self) do
|
12
|
-
@
|
12
|
+
@filters_map = case definition
|
13
13
|
when Hash
|
14
14
|
definition
|
15
15
|
when Array
|
@@ -18,7 +18,7 @@ module Praxis
|
|
18
18
|
raise "Cannot use FilterQueryBuilder.of without passing an array or a hash (Got: #{definition.class.name})"
|
19
19
|
end
|
20
20
|
class << self
|
21
|
-
attr_reader :
|
21
|
+
attr_reader :filters_map
|
22
22
|
end
|
23
23
|
end
|
24
24
|
end
|
@@ -33,13 +33,18 @@ module Praxis
|
|
33
33
|
end
|
34
34
|
|
35
35
|
# By default we'll simply use the incoming op and value, and will map
|
36
|
-
# the attribute based on what's on the `
|
36
|
+
# the attribute based on what's on the `filters_map` definition
|
37
37
|
def generate(filters)
|
38
38
|
raise "Not refactored yet!"
|
39
39
|
seen_associations = Set.new
|
40
40
|
filters.each do |(attr, spec)|
|
41
|
-
column_name =
|
42
|
-
|
41
|
+
column_name = _mapped_filter(attr)
|
42
|
+
unless column_name
|
43
|
+
msg = "Filtering by #{attr} is not allowed. No implementation mapping defined for it has been found \
|
44
|
+
and there is not a model attribute with this name either.\n" \
|
45
|
+
"Please add a mapping for #{attr} in the `filters_mapping` method of the appropriate Resource class"
|
46
|
+
raise msg
|
47
|
+
end
|
43
48
|
if column_name.is_a?(Proc)
|
44
49
|
bindings = column_name.call(spec)
|
45
50
|
# A hash of bindings, consisting of a key with column name and a value to the query value
|
@@ -64,9 +69,16 @@ module Praxis
|
|
64
69
|
add_clause(attr: column_name, op: op, value: value)
|
65
70
|
end
|
66
71
|
|
67
|
-
def
|
68
|
-
|
69
|
-
|
72
|
+
def _mapped_filter(name)
|
73
|
+
target = self.class.filters_map[name]
|
74
|
+
unless target
|
75
|
+
if @model.attribute_names.include?(name.to_s)
|
76
|
+
# Cache it in the filters mapping (to avoid later lookups), and return it.
|
77
|
+
self.class.filters_map[name] = name
|
78
|
+
target = name
|
79
|
+
end
|
80
|
+
end
|
81
|
+
return target
|
70
82
|
end
|
71
83
|
|
72
84
|
# Private to try to funnel all column names through `generate` that restricts
|
@@ -14,10 +14,9 @@ module Praxis
|
|
14
14
|
module Pagination
|
15
15
|
extend ActiveSupport::Concern
|
16
16
|
# This PaginatedController concern should be added to controllers that have actions that define the
|
17
|
-
# pagination and order parameters so that
|
18
|
-
#
|
19
|
-
# This
|
20
|
-
# can be easily applied to other chainable query proxies.
|
17
|
+
# pagination and order parameters so that one can call the domain model to craft the query
|
18
|
+
# `domain_model.craft_pagination_query(base_query, pagination: _pagination)`
|
19
|
+
# This will handle all the required logic for paginating, ordering and generating the Link and TotalCount headers.
|
21
20
|
#
|
22
21
|
# Here's a simple example on how to use it for a fake Items controller
|
23
22
|
# class Items < V1::Controllers::BaseController
|
@@ -29,7 +28,8 @@ module Praxis
|
|
29
28
|
#
|
30
29
|
# def index(filters: nil, pagination: nil, order: nil, **_args)
|
31
30
|
# items = current_user.items.all
|
32
|
-
#
|
31
|
+
# domain_model = self.media_type.domain_model
|
32
|
+
# items = domain_model.craft_pagination_query( query: items, pagination: _pagination)
|
33
33
|
#
|
34
34
|
# display(items)
|
35
35
|
# end
|
@@ -71,33 +71,6 @@ module Praxis
|
|
71
71
|
@_pagination = PaginationStruct.new(pagination[:paginator], pagination[:order])
|
72
72
|
end
|
73
73
|
|
74
|
-
# Main entrypoint: Handles all pagination pieces
|
75
|
-
# takes:
|
76
|
-
# * the query to build from and the table
|
77
|
-
# * the request (for link header generation)
|
78
|
-
# * requires the _pagination variable to be there (set by this module) to return the pagination struct
|
79
|
-
def _craft_pagination_query(query:, type: :active_record)
|
80
|
-
handler_klass = \
|
81
|
-
case type
|
82
|
-
when :active_record
|
83
|
-
ActiveRecordPaginationHandler
|
84
|
-
when :sequel
|
85
|
-
SequelPaginationHandler
|
86
|
-
else
|
87
|
-
raise "Attempting to use pagination but Active Record or Sequel gems found"
|
88
|
-
end
|
89
|
-
|
90
|
-
# Gather and save the count if required
|
91
|
-
if _pagination.paginator&.total_count
|
92
|
-
_pagination.total_count = handler_klass.count(query.dup)
|
93
|
-
end
|
94
|
-
|
95
|
-
query = handler_klass.order(query, _pagination.order)
|
96
|
-
# Maybe this is a class instance instead of a class method?...(of the appropriate AR/Sequel type)...
|
97
|
-
# self.class.paginate(query, table, _pagination)
|
98
|
-
handler_klass.paginate(query, _pagination)
|
99
|
-
end
|
100
|
-
|
101
74
|
def build_pagination_headers(pagination:, current_url:, current_query_params:)
|
102
75
|
links = if pagination.paginator.by
|
103
76
|
# We're assuming that the last element has a "symbol/string" field with the same name of the "by" pagination.
|