praxis 2.0.pre.10 → 2.0.pre.15
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/.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.
|