brainstem 1.0.0 → 1.1.0
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 +3 -0
- data/Gemfile.lock +37 -32
- data/README.md +15 -0
- data/brainstem.gemspec +1 -0
- data/lib/brainstem/presenter_collection.rb +18 -6
- data/lib/brainstem/query_strategies/base_strategy.rb +33 -6
- data/lib/brainstem/query_strategies/filter_and_search.rb +38 -17
- data/lib/brainstem/query_strategies/filter_or_search.rb +1 -23
- data/lib/brainstem/version.rb +1 -1
- data/spec/brainstem/presenter_collection_spec.rb +67 -8
- data/spec/brainstem/query_strategies/filter_and_search_spec.rb +136 -15
- data/spec/brainstem/query_strategies/filter_or_search_spec.rb +2 -0
- data/spec/shared/base_strategy.rb +36 -0
- data/spec/spec_helper.rb +1 -0
- metadata +19 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9bf2b0cdf9ecd18e7a90f8484238fe6e7f63ad43
|
|
4
|
+
data.tar.gz: 7be99f891a90e26715f553fcf0971931bf51c404
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 138e7db36bf3692bde7fac6c61777ea8695195d32769dec30555bc2d4b5124679dd8f22e08624503ba3148e64c058bcad31f72b24842f2bf864bb4291ca8e052
|
|
7
|
+
data.tar.gz: 37760c56b41a4fbf8dbb5e19b543229c878e61fd88a7a8400f961140565b70c9b738b1882b7aeee1dc3c41e651695e02ab43ed95efd1483a80eafbdf38abf50c
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
+ **1.1.0 - _12/18/2017_
|
|
4
|
+
- Add `meta` key to API responses which includes `page_number`, `page_count`, and `page_size` keys.
|
|
5
|
+
|
|
3
6
|
+ **1.0.0 - _07/20/2017_
|
|
4
7
|
- Add the capability to generate the documentation extracted from your properly annotated
|
|
5
8
|
presenters and controllers using `bundle exec brainstem generate [ARGS]`.
|
data/Gemfile.lock
CHANGED
|
@@ -1,60 +1,64 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
brainstem (1.
|
|
4
|
+
brainstem (1.1.0)
|
|
5
5
|
activerecord (>= 4.1)
|
|
6
6
|
activesupport (>= 4.1)
|
|
7
7
|
|
|
8
8
|
GEM
|
|
9
9
|
remote: https://rubygems.org/
|
|
10
10
|
specs:
|
|
11
|
-
activemodel (5.
|
|
12
|
-
activesupport (= 5.
|
|
13
|
-
activerecord (5.
|
|
14
|
-
activemodel (= 5.
|
|
15
|
-
activesupport (= 5.
|
|
16
|
-
arel (~>
|
|
17
|
-
activesupport (5.
|
|
11
|
+
activemodel (5.1.4)
|
|
12
|
+
activesupport (= 5.1.4)
|
|
13
|
+
activerecord (5.1.4)
|
|
14
|
+
activemodel (= 5.1.4)
|
|
15
|
+
activesupport (= 5.1.4)
|
|
16
|
+
arel (~> 8.0)
|
|
17
|
+
activesupport (5.1.4)
|
|
18
18
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
|
19
19
|
i18n (~> 0.7)
|
|
20
20
|
minitest (~> 5.1)
|
|
21
21
|
tzinfo (~> 1.1)
|
|
22
|
-
arel (
|
|
23
|
-
coderay (1.1.
|
|
22
|
+
arel (8.0.0)
|
|
23
|
+
coderay (1.1.2)
|
|
24
24
|
concurrent-ruby (1.0.5)
|
|
25
|
-
database_cleaner (1.
|
|
25
|
+
database_cleaner (1.6.2)
|
|
26
|
+
db-query-matchers (0.9.0)
|
|
27
|
+
activesupport (>= 4.0, <= 6.0)
|
|
28
|
+
rspec (~> 3.0)
|
|
26
29
|
diff-lcs (1.3)
|
|
27
|
-
i18n (0.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
i18n (0.9.1)
|
|
31
|
+
concurrent-ruby (~> 1.0)
|
|
32
|
+
method_source (0.9.0)
|
|
33
|
+
minitest (5.10.3)
|
|
34
|
+
pry (0.9.12.6)
|
|
35
|
+
coderay (~> 1.0)
|
|
36
|
+
method_source (~> 0.8)
|
|
33
37
|
slop (~> 3.4)
|
|
34
38
|
pry-nav (0.2.4)
|
|
35
39
|
pry (>= 0.9.10, < 0.11.0)
|
|
36
|
-
rake (12.
|
|
40
|
+
rake (12.2.1)
|
|
37
41
|
redcarpet (3.4.0)
|
|
38
|
-
rr (1.2.
|
|
39
|
-
rspec (3.
|
|
40
|
-
rspec-core (~> 3.
|
|
41
|
-
rspec-expectations (~> 3.
|
|
42
|
-
rspec-mocks (~> 3.
|
|
43
|
-
rspec-core (3.
|
|
44
|
-
rspec-support (~> 3.
|
|
45
|
-
rspec-expectations (3.
|
|
42
|
+
rr (1.2.1)
|
|
43
|
+
rspec (3.7.0)
|
|
44
|
+
rspec-core (~> 3.7.0)
|
|
45
|
+
rspec-expectations (~> 3.7.0)
|
|
46
|
+
rspec-mocks (~> 3.7.0)
|
|
47
|
+
rspec-core (3.7.0)
|
|
48
|
+
rspec-support (~> 3.7.0)
|
|
49
|
+
rspec-expectations (3.7.0)
|
|
46
50
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
47
|
-
rspec-support (~> 3.
|
|
48
|
-
rspec-mocks (3.
|
|
51
|
+
rspec-support (~> 3.7.0)
|
|
52
|
+
rspec-mocks (3.7.0)
|
|
49
53
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
50
|
-
rspec-support (~> 3.
|
|
51
|
-
rspec-support (3.
|
|
54
|
+
rspec-support (~> 3.7.0)
|
|
55
|
+
rspec-support (3.7.0)
|
|
52
56
|
slop (3.6.0)
|
|
53
57
|
sqlite3 (1.3.13)
|
|
54
58
|
thread_safe (0.3.6)
|
|
55
|
-
tzinfo (1.2.
|
|
59
|
+
tzinfo (1.2.4)
|
|
56
60
|
thread_safe (~> 0.1)
|
|
57
|
-
yard (0.9.
|
|
61
|
+
yard (0.9.9)
|
|
58
62
|
|
|
59
63
|
PLATFORMS
|
|
60
64
|
ruby
|
|
@@ -62,6 +66,7 @@ PLATFORMS
|
|
|
62
66
|
DEPENDENCIES
|
|
63
67
|
brainstem!
|
|
64
68
|
database_cleaner
|
|
69
|
+
db-query-matchers
|
|
65
70
|
pry
|
|
66
71
|
pry-nav
|
|
67
72
|
rake
|
data/README.md
CHANGED
|
@@ -216,6 +216,21 @@ Responses will look like the following:
|
|
|
216
216
|
# Total number of results that matched the query.
|
|
217
217
|
count: 5,
|
|
218
218
|
|
|
219
|
+
# Information about the request and response.
|
|
220
|
+
meta: {
|
|
221
|
+
# Total number of results that matched the query.
|
|
222
|
+
count: 5,
|
|
223
|
+
|
|
224
|
+
# Current page returned in the response.
|
|
225
|
+
page_number: 1,
|
|
226
|
+
|
|
227
|
+
# Total number pages available.
|
|
228
|
+
page_count: 1,
|
|
229
|
+
|
|
230
|
+
# Number of results per page.
|
|
231
|
+
page_size: 20,
|
|
232
|
+
},
|
|
233
|
+
|
|
219
234
|
# A lookup table to top-level keys. Necessary
|
|
220
235
|
# because some objects can have associations of
|
|
221
236
|
# the same type as themselves. Also helps to
|
data/brainstem.gemspec
CHANGED
|
@@ -53,17 +53,18 @@ module Brainstem
|
|
|
53
53
|
options[:default_max_per_page] = default_max_per_page
|
|
54
54
|
options[:default_max_filter_and_search_page] = default_max_filter_and_search_page
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
strategy = get_strategy(options, scope)
|
|
57
|
+
primary_models, count = strategy.execute(scope)
|
|
57
58
|
|
|
58
59
|
# Determine if an exception should be raised on an empty result set.
|
|
59
60
|
if options[:raise_on_empty] && primary_models.empty?
|
|
60
61
|
raise options[:empty_error_class] || ActiveRecord::RecordNotFound
|
|
61
62
|
end
|
|
62
63
|
|
|
63
|
-
structure_response(presented_class, primary_models, count, options)
|
|
64
|
+
structure_response(presented_class, primary_models, strategy, count, options)
|
|
64
65
|
end
|
|
65
66
|
|
|
66
|
-
def structure_response(presented_class, primary_models, count, options)
|
|
67
|
+
def structure_response(presented_class, primary_models, strategy, count, options)
|
|
67
68
|
# key these models will use in the struct that is output
|
|
68
69
|
brainstem_key = brainstem_key_for!(presented_class)
|
|
69
70
|
|
|
@@ -71,8 +72,19 @@ module Brainstem
|
|
|
71
72
|
selected_associations = filter_includes(options)
|
|
72
73
|
|
|
73
74
|
optional_fields = filter_optional_fields(options)
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
page_size = strategy.calculate_per_page
|
|
76
|
+
|
|
77
|
+
struct = {
|
|
78
|
+
'count' => count,
|
|
79
|
+
'results' => [],
|
|
80
|
+
brainstem_key => {},
|
|
81
|
+
'meta' => {
|
|
82
|
+
'count' => count,
|
|
83
|
+
'page_count' => count > 0 ? (count.to_f / page_size).ceil : 0,
|
|
84
|
+
'page_number' => count > 0 ? options[:params].fetch(:page, 1).to_i : 0,
|
|
85
|
+
'page_size' => page_size,
|
|
86
|
+
}
|
|
87
|
+
}
|
|
76
88
|
|
|
77
89
|
# Build top-level keys for all requested associations.
|
|
78
90
|
selected_associations.each do |association|
|
|
@@ -145,7 +157,7 @@ module Brainstem
|
|
|
145
157
|
|
|
146
158
|
private
|
|
147
159
|
|
|
148
|
-
def
|
|
160
|
+
def get_strategy(options, scope)
|
|
149
161
|
strat = options[:primary_presenter].get_query_strategy
|
|
150
162
|
|
|
151
163
|
return Brainstem::QueryStrategies::FilterAndSearch.new(options) if strat == :filter_and_search && searching?(options)
|
|
@@ -26,6 +26,12 @@ module Brainstem
|
|
|
26
26
|
end
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
+
def calculate_per_page
|
|
30
|
+
per_page = [(@options[:params][:per_page] || @options[:per_page] || @options[:default_per_page]).to_i, (@options[:max_per_page] || @options[:default_max_per_page]).to_i].min
|
|
31
|
+
per_page = @options[:default_per_page] if per_page < 1
|
|
32
|
+
per_page
|
|
33
|
+
end
|
|
34
|
+
|
|
29
35
|
private
|
|
30
36
|
|
|
31
37
|
def calculate_limit
|
|
@@ -36,12 +42,6 @@ module Brainstem
|
|
|
36
42
|
[@options[:params][:offset].to_i, 0].max
|
|
37
43
|
end
|
|
38
44
|
|
|
39
|
-
def calculate_per_page
|
|
40
|
-
per_page = [(@options[:params][:per_page] || @options[:per_page] || @options[:default_per_page]).to_i, (@options[:max_per_page] || @options[:default_max_per_page]).to_i].min
|
|
41
|
-
per_page = @options[:default_per_page] if per_page < 1
|
|
42
|
-
per_page
|
|
43
|
-
end
|
|
44
|
-
|
|
45
45
|
def calculate_page
|
|
46
46
|
[(@options[:params][:page] || 1).to_i, 1].max
|
|
47
47
|
end
|
|
@@ -57,6 +57,33 @@ module Brainstem
|
|
|
57
57
|
end
|
|
58
58
|
end
|
|
59
59
|
end
|
|
60
|
+
|
|
61
|
+
def order_for_search(records, ordered_search_ids, options = {})
|
|
62
|
+
ids_to_position = {}
|
|
63
|
+
ordered_records = []
|
|
64
|
+
|
|
65
|
+
ordered_search_ids.each_with_index do |id, index|
|
|
66
|
+
ids_to_position[id] = index
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
records.each do |record|
|
|
70
|
+
ordered_records[ids_to_position[options[:with_ids] ? record : record.id]] = record
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
ordered_records.compact
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def calculate_limit_and_offset
|
|
77
|
+
if @options[:params][:limit].present? && @options[:params][:offset].present?
|
|
78
|
+
limit = calculate_limit
|
|
79
|
+
offset = calculate_offset
|
|
80
|
+
else
|
|
81
|
+
limit = calculate_per_page
|
|
82
|
+
offset = limit * (calculate_page - 1)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
[limit, offset]
|
|
86
|
+
end
|
|
60
87
|
end
|
|
61
88
|
end
|
|
62
89
|
end
|
|
@@ -2,13 +2,31 @@ module Brainstem
|
|
|
2
2
|
module QueryStrategies
|
|
3
3
|
class FilterAndSearch < BaseStrategy
|
|
4
4
|
def execute(scope)
|
|
5
|
-
ordered_search_ids = run_search(scope, filter_includes.map(&:name))
|
|
6
|
-
scope = scope.where(id: ordered_search_ids)
|
|
5
|
+
scope, ordered_search_ids = run_search(scope, filter_includes.map(&:name))
|
|
7
6
|
scope = @options[:primary_presenter].apply_filters_to_scope(scope, @options[:params], @options)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
|
|
8
|
+
if ordering?
|
|
9
|
+
count = scope.count
|
|
10
|
+
scope = paginate(scope)
|
|
11
|
+
scope = @options[:primary_presenter].apply_ordering_to_scope(scope, @options[:params])
|
|
12
|
+
primary_models = evaluate_scope(scope)
|
|
13
|
+
else
|
|
14
|
+
filtered_ids = scope.pluck(:id)
|
|
15
|
+
count = filtered_ids.size
|
|
16
|
+
|
|
17
|
+
# order a potentially large set of ids
|
|
18
|
+
ordered_ids = order_for_search(filtered_ids, ordered_search_ids, with_ids: true)
|
|
19
|
+
ordered_paginated_ids = paginate_array(ordered_ids)
|
|
20
|
+
|
|
21
|
+
scope = scope.unscoped.where(id: ordered_paginated_ids)
|
|
22
|
+
# not using `evaluate_scope` because we are already instantiating
|
|
23
|
+
# a scope based on ids
|
|
24
|
+
primary_models = scope.to_a
|
|
25
|
+
|
|
26
|
+
# Once hydrated, a page worth of models needs to be reordered
|
|
27
|
+
# due to the `scope.unscoped.where(id: ...` clobbering our ordering
|
|
28
|
+
primary_models = order_for_search(primary_models, ordered_paginated_ids)
|
|
29
|
+
end
|
|
12
30
|
|
|
13
31
|
[primary_models, count]
|
|
14
32
|
end
|
|
@@ -16,10 +34,8 @@ module Brainstem
|
|
|
16
34
|
private
|
|
17
35
|
|
|
18
36
|
def run_search(scope, includes)
|
|
19
|
-
sort_name, direction = @options[:primary_presenter].calculate_sort_name_and_direction @options[:params]
|
|
20
37
|
search_options = HashWithIndifferentAccess.new(
|
|
21
38
|
include: includes,
|
|
22
|
-
order: { sort_order: sort_name, direction: direction },
|
|
23
39
|
limit: @options[:default_max_filter_and_search_page],
|
|
24
40
|
offset: 0
|
|
25
41
|
)
|
|
@@ -28,23 +44,28 @@ module Brainstem
|
|
|
28
44
|
|
|
29
45
|
result_ids, _ = @options[:primary_presenter].run_search(@options[:params][:search], search_options)
|
|
30
46
|
if result_ids
|
|
31
|
-
result_ids
|
|
47
|
+
resulting_scope = scope.where(id: result_ids)
|
|
48
|
+
[resulting_scope, result_ids]
|
|
32
49
|
else
|
|
33
50
|
raise(SearchUnavailableError, 'Search is currently unavailable')
|
|
34
51
|
end
|
|
35
52
|
end
|
|
36
53
|
|
|
37
|
-
def
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
limit = calculate_per_page
|
|
43
|
-
offset = limit * (calculate_page - 1)
|
|
44
|
-
end
|
|
54
|
+
def ordering?
|
|
55
|
+
sort_name = @options[:params][:order].to_s.split(":").first
|
|
56
|
+
sort_orders = @options[:primary_presenter].configuration[:sort_orders]
|
|
57
|
+
sort_name.present? && sort_orders && sort_orders[sort_name].present?
|
|
58
|
+
end
|
|
45
59
|
|
|
60
|
+
def paginate(scope)
|
|
61
|
+
limit, offset = calculate_limit_and_offset
|
|
46
62
|
scope.limit(limit).offset(offset).distinct
|
|
47
63
|
end
|
|
64
|
+
|
|
65
|
+
def paginate_array(array)
|
|
66
|
+
limit, offset = calculate_limit_and_offset
|
|
67
|
+
array.drop(offset).first(limit) # do we need to uniq this?
|
|
68
|
+
end
|
|
48
69
|
end
|
|
49
70
|
end
|
|
50
71
|
end
|
|
@@ -68,14 +68,7 @@ module Brainstem
|
|
|
68
68
|
end
|
|
69
69
|
|
|
70
70
|
def paginate(scope)
|
|
71
|
-
|
|
72
|
-
limit = calculate_limit
|
|
73
|
-
offset = calculate_offset
|
|
74
|
-
else
|
|
75
|
-
limit = calculate_per_page
|
|
76
|
-
offset = limit * (calculate_page - 1)
|
|
77
|
-
end
|
|
78
|
-
|
|
71
|
+
limit, offset = calculate_limit_and_offset
|
|
79
72
|
[scope.limit(limit).offset(offset).distinct, scope.select("distinct #{scope.connection.quote_table_name @options[:table_name]}.id").count]
|
|
80
73
|
end
|
|
81
74
|
|
|
@@ -83,21 +76,6 @@ module Brainstem
|
|
|
83
76
|
ids = (only || "").split(",").select {|id| id =~ /\A\d+\z/}.uniq
|
|
84
77
|
[scope.where(:id => ids), scope.where(:id => ids).count]
|
|
85
78
|
end
|
|
86
|
-
|
|
87
|
-
def order_for_search(records, ordered_search_ids)
|
|
88
|
-
ids_to_position = {}
|
|
89
|
-
ordered_records = []
|
|
90
|
-
|
|
91
|
-
ordered_search_ids.each_with_index do |id, index|
|
|
92
|
-
ids_to_position[id] = index
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
records.each do |record|
|
|
96
|
-
ordered_records[ids_to_position[record.id]] = record
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
ordered_records.compact
|
|
100
|
-
end
|
|
101
79
|
end
|
|
102
80
|
end
|
|
103
81
|
end
|
data/lib/brainstem/version.rb
CHANGED
|
@@ -10,7 +10,7 @@ describe Brainstem::PresenterCollection do
|
|
|
10
10
|
let(:jane) { User.where(:username => "jane").first }
|
|
11
11
|
|
|
12
12
|
describe "#presenting" do
|
|
13
|
-
describe "
|
|
13
|
+
describe "pagination" do
|
|
14
14
|
before do
|
|
15
15
|
@presenter_collection.default_per_page = 2
|
|
16
16
|
@presenter_collection.default_max_per_page = 3
|
|
@@ -159,6 +159,64 @@ describe Brainstem::PresenterCollection do
|
|
|
159
159
|
expect(result['count']).to eq(Workspace.count)
|
|
160
160
|
end
|
|
161
161
|
end
|
|
162
|
+
|
|
163
|
+
describe "meta keys" do
|
|
164
|
+
let(:max_per_page) { nil }
|
|
165
|
+
let(:meta_keys) { result["meta"] }
|
|
166
|
+
let(:result) { @presenter_collection.presenting("workspaces", params: params, max_per_page: max_per_page) { Workspace.unscoped } }
|
|
167
|
+
|
|
168
|
+
describe "count" do
|
|
169
|
+
let(:params) { {} }
|
|
170
|
+
|
|
171
|
+
it "includes the count" do
|
|
172
|
+
expect(meta_keys["count"]).to eq(6)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
describe "page_number" do
|
|
177
|
+
describe "when page is provided" do
|
|
178
|
+
let(:params) { { page: 2 } }
|
|
179
|
+
|
|
180
|
+
it "indicates the provided page" do
|
|
181
|
+
expect(meta_keys["page_number"]).to eq(2)
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
describe "when page is not profided" do
|
|
186
|
+
let(:params) { {} }
|
|
187
|
+
|
|
188
|
+
it "indicates the first page" do
|
|
189
|
+
expect(meta_keys["page_number"]).to eq(1)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
describe "page_count" do
|
|
195
|
+
describe "when per_page is provided" do
|
|
196
|
+
let(:params) { { per_page: 4 } }
|
|
197
|
+
|
|
198
|
+
it "calculates the correct page count" do
|
|
199
|
+
expect(meta_keys["page_count"]).to eq(2)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
describe "when per_page is not provided" do
|
|
204
|
+
let(:params) { {} }
|
|
205
|
+
|
|
206
|
+
it "calculates the correct page count based on the default page size" do
|
|
207
|
+
expect(meta_keys["page_count"]).to eq(3)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
describe "page_size" do
|
|
213
|
+
let(:params) { {} }
|
|
214
|
+
|
|
215
|
+
it "calculates the correct page size" do
|
|
216
|
+
expect(meta_keys["page_count"]).to eq(3)
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
162
220
|
end
|
|
163
221
|
|
|
164
222
|
describe 'strategies' do
|
|
@@ -256,7 +314,7 @@ describe Brainstem::PresenterCollection do
|
|
|
256
314
|
describe "the 'results' top level key" do
|
|
257
315
|
it "comes back with an explicit list of the matching results" do
|
|
258
316
|
structure = @presenter_collection.presenting("workspaces", :params => { :include => "tasks" }, :max_per_page => 2) { Workspace.where(:id => 1) }
|
|
259
|
-
expect(structure.keys).to match_array %w[workspaces tasks count results]
|
|
317
|
+
expect(structure.keys).to match_array %w[workspaces tasks count results meta]
|
|
260
318
|
expect(structure['results']).to eq(Workspace.where(:id => 1).limit(2).map {|w| { 'key' => 'workspaces', 'id' => w.id.to_s } })
|
|
261
319
|
expect(structure['workspaces'].keys).to eq(%w[1])
|
|
262
320
|
end
|
|
@@ -265,10 +323,10 @@ describe Brainstem::PresenterCollection do
|
|
|
265
323
|
describe "includes" do
|
|
266
324
|
it "reads allowed includes from the presenter" do
|
|
267
325
|
result = @presenter_collection.presenting("workspaces", :params => { :include => "drop table,tasks,users" }) { Workspace.unscoped }
|
|
268
|
-
expect(result.keys).to match_array %w[count workspaces tasks results]
|
|
326
|
+
expect(result.keys).to match_array %w[count meta workspaces tasks results]
|
|
269
327
|
|
|
270
328
|
result = @presenter_collection.presenting("workspaces", :params => { :include => "foo,tasks,lead_user" }) { Workspace.unscoped }
|
|
271
|
-
expect(result.keys).to match_array %w[count workspaces tasks users results]
|
|
329
|
+
expect(result.keys).to match_array %w[count meta workspaces tasks users results]
|
|
272
330
|
end
|
|
273
331
|
|
|
274
332
|
it "defaults to not include any allowed includes" do
|
|
@@ -304,7 +362,7 @@ describe Brainstem::PresenterCollection do
|
|
|
304
362
|
result = @presenter_collection.presenting("tasks", :params => { :include => "workspace" }, :max_per_page => 2) { Task.where(:id => t.id) }
|
|
305
363
|
expect(result['tasks'].keys).to eq([ t.id.to_s ])
|
|
306
364
|
expect(result['workspaces']).to eq({})
|
|
307
|
-
expect(result.keys).to match_array %w[tasks workspaces count results]
|
|
365
|
+
expect(result.keys).to match_array %w[tasks workspaces count meta results]
|
|
308
366
|
end
|
|
309
367
|
|
|
310
368
|
context 'when including something of the same type as the primary model' do
|
|
@@ -911,7 +969,7 @@ describe Brainstem::PresenterCollection do
|
|
|
911
969
|
context "when there is no sort provided" do
|
|
912
970
|
it "returns an empty array when there are no objects" do
|
|
913
971
|
result = @presenter_collection.presenting("workspaces") { Workspace.where(:id => nil) }
|
|
914
|
-
expect(result).to eq('count' => 0, 'workspaces' => {}, 'results' => [])
|
|
972
|
+
expect(result).to eq('count' => 0, 'meta' => { 'count' => 0, 'page_count' => 0, 'page_number' => 0, 'page_size' => 20 }, 'workspaces' => {}, 'results' => [])
|
|
915
973
|
end
|
|
916
974
|
|
|
917
975
|
it "falls back to the object's sort order when nothing is provided" do
|
|
@@ -966,7 +1024,7 @@ describe Brainstem::PresenterCollection do
|
|
|
966
1024
|
|
|
967
1025
|
result = @presenter_collection.presenting("workspaces", :params => { :order => "description:drop table" }) { Workspace.where("id is not null") }
|
|
968
1026
|
expect(last_direction).to eq('asc')
|
|
969
|
-
expect(result.keys).to match_array %w[count workspaces results]
|
|
1027
|
+
expect(result.keys).to match_array %w[count meta workspaces results]
|
|
970
1028
|
|
|
971
1029
|
result = @presenter_collection.presenting("workspaces", :params => { :order => "description:;;hacker;;" }) { Workspace.where("id is not null") }
|
|
972
1030
|
expect(last_direction).to eq('asc')
|
|
@@ -1087,7 +1145,8 @@ describe Brainstem::PresenterCollection do
|
|
|
1087
1145
|
|
|
1088
1146
|
describe '#structure_response' do
|
|
1089
1147
|
let(:options) { {params: {}, primary_presenter: @presenter_collection.for!(Workspace) } }
|
|
1090
|
-
let(:response_body) { @presenter_collection.structure_response(Workspace, Workspace.all, 17, options) }
|
|
1148
|
+
let(:response_body) { @presenter_collection.structure_response(Workspace, Workspace.all, strategy, 17, options) }
|
|
1149
|
+
let(:strategy) { OpenStruct.new(calculate_per_page: 25) }
|
|
1091
1150
|
|
|
1092
1151
|
it 'has a count' do
|
|
1093
1152
|
expect(response_body['count']).to eq(17)
|
|
@@ -2,11 +2,10 @@ require 'spec_helper'
|
|
|
2
2
|
|
|
3
3
|
describe Brainstem::QueryStrategies::FilterAndSearch do
|
|
4
4
|
let(:bob) { User.find_by_username('bob') }
|
|
5
|
+
let(:jane) { User.find_by_username('jane') }
|
|
5
6
|
|
|
6
|
-
let(:
|
|
7
|
+
let(:default_params) { {
|
|
7
8
|
"owned_by" => bob.id.to_s,
|
|
8
|
-
per_page: 7,
|
|
9
|
-
page: 1,
|
|
10
9
|
search: 'toot, otto, toot',
|
|
11
10
|
} }
|
|
12
11
|
|
|
@@ -19,28 +18,150 @@ describe Brainstem::QueryStrategies::FilterAndSearch do
|
|
|
19
18
|
params: params
|
|
20
19
|
} }
|
|
21
20
|
|
|
21
|
+
it_behaves_like Brainstem::QueryStrategies::BaseStrategy
|
|
22
|
+
|
|
22
23
|
describe '#execute' do
|
|
24
|
+
def run_query
|
|
25
|
+
described_class.new(options).execute(Cheese.all)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
let(:owned_by_bob) { Cheese.owned_by(bob.id)}
|
|
29
|
+
let(:owned_by_jane) { Cheese.owned_by(jane.id)}
|
|
30
|
+
|
|
23
31
|
before do
|
|
24
|
-
|
|
25
|
-
|
|
32
|
+
@search_results = search_results = Cheese.all.pluck(:id).shuffle
|
|
33
|
+
|
|
34
|
+
CheesePresenter.search do
|
|
35
|
+
[search_results, search_results.count]
|
|
26
36
|
end
|
|
27
37
|
|
|
28
38
|
CheesePresenter.filter(:owned_by) { |scope, user_id| scope.owned_by(user_id.to_i) }
|
|
29
39
|
CheesePresenter.sort_order(:id) { |scope, direction| scope.order("cheeses.id #{direction}") }
|
|
30
40
|
end
|
|
31
41
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
42
|
+
context 'when an order is specified' do
|
|
43
|
+
let(:order) { 'id:asc' }
|
|
44
|
+
let(:params) { default_params.merge({ order: order })}
|
|
45
|
+
let(:expected_ordered_ids) { owned_by_bob.order("cheeses.id ASC").pluck(:id) }
|
|
46
|
+
|
|
47
|
+
it 'returns the filtered, ordered search results' do
|
|
48
|
+
results, count = run_query
|
|
49
|
+
expect(count).to eq(owned_by_bob.count)
|
|
50
|
+
expect(results.map(&:id)).to eq(expected_ordered_ids)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
context 'with limit and offset params' do
|
|
54
|
+
let(:limit) { 2 }
|
|
55
|
+
let(:offset) { 4 }
|
|
56
|
+
let(:params) { default_params.merge({ order: order, limit: limit, offset: offset })}
|
|
57
|
+
let(:expected_paginated_ids) { expected_ordered_ids.drop(offset).first(limit) }
|
|
58
|
+
|
|
59
|
+
it 'returns the filtered, ordered, paginated results' do
|
|
60
|
+
results, count = run_query
|
|
61
|
+
expect(count).to eq(owned_by_bob.count)
|
|
62
|
+
expect(results.map(&:id)).to eq(expected_paginated_ids)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
context 'with page and per_page params' do
|
|
67
|
+
let(:page) { 2 }
|
|
68
|
+
let(:per_page) { 3 }
|
|
69
|
+
let(:params) { default_params.merge({ order: order, page: page, per_page: per_page })}
|
|
70
|
+
let(:expected_paginated_ids) { expected_ordered_ids.drop(per_page).first(per_page) }
|
|
71
|
+
|
|
72
|
+
it 'returns the filtered, ordered, paginated results' do
|
|
73
|
+
results, count = run_query
|
|
74
|
+
expect(count).to eq(owned_by_bob.count)
|
|
75
|
+
expect(results.map(&:id)).to eq(expected_paginated_ids)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
context 'when no order is specified' do
|
|
81
|
+
let(:params) { default_params }
|
|
82
|
+
let(:expected_ordered_ids) { @search_results - owned_by_jane.pluck(:id) }
|
|
83
|
+
|
|
84
|
+
before do
|
|
85
|
+
expect(params[:order]).not_to be_present
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it 'returns the filtered results ordered by search' do
|
|
89
|
+
results, count = run_query
|
|
90
|
+
expect(count).to eq(owned_by_bob.count)
|
|
91
|
+
expect(results.map(&:id)).to eq(expected_ordered_ids)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it 'only does two database queries' do
|
|
95
|
+
expect { run_query }.to make_database_queries(count: 2)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
context 'with limit and offset params' do
|
|
99
|
+
let(:limit) { 2 }
|
|
100
|
+
let(:offset) { 4 }
|
|
101
|
+
let(:params) { default_params.merge({ limit: limit, offset: offset })}
|
|
102
|
+
let(:expected_paginated_ids) { expected_ordered_ids.drop(offset).first(limit) }
|
|
103
|
+
|
|
104
|
+
it 'returns the filtered, ordered, paginated results' do
|
|
105
|
+
results, count = run_query
|
|
106
|
+
expect(count).to eq(owned_by_bob.count)
|
|
107
|
+
expect(results.map(&:id)).to eq(expected_paginated_ids)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
context 'with page and per_page params' do
|
|
112
|
+
let(:page) { 2 }
|
|
113
|
+
let(:per_page) { 3 }
|
|
114
|
+
let(:params) { default_params.merge({ page: page, per_page: per_page })}
|
|
115
|
+
let(:expected_paginated_ids) { expected_ordered_ids.drop(per_page).first(per_page) }
|
|
116
|
+
|
|
117
|
+
it 'returns the filtered, ordered, paginated results' do
|
|
118
|
+
results, count = run_query
|
|
119
|
+
expect(count).to eq(owned_by_bob.count)
|
|
120
|
+
expect(results.map(&:id)).to eq(expected_paginated_ids)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
describe '#ordering?' do
|
|
127
|
+
context 'when the order param is passed' do
|
|
128
|
+
let(:params) { default_params.merge({ order: 'canadianness' })}
|
|
129
|
+
|
|
130
|
+
context 'and it exists on the presenter' do
|
|
131
|
+
before do
|
|
132
|
+
CheesePresenter.sort_order(:canadianness) { |scope, direction| scope.order("cheeses.hockey #{direction}") }
|
|
133
|
+
expect(CheesePresenter.configuration[:sort_orders][:canadianness]).to be_present
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
it 'returns true' do
|
|
137
|
+
query_strat = described_class.new(options)
|
|
138
|
+
expect(query_strat.send(:ordering?)).to eq(true)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
context 'and it does not exist on the presenter' do
|
|
143
|
+
before do
|
|
144
|
+
expect(CheesePresenter.configuration[:sort_orders][:canadianness]).not_to be_present
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
it 'returns false' do
|
|
148
|
+
query_strat = described_class.new(options)
|
|
149
|
+
expect(query_strat.send(:ordering?)).to eq(false)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
36
152
|
end
|
|
37
153
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
154
|
+
context 'when the order param is not passed' do
|
|
155
|
+
let(:params) { default_params }
|
|
156
|
+
|
|
157
|
+
before do
|
|
158
|
+
expect(params[:order]).not_to be_present
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
it 'returns false' do
|
|
162
|
+
query_strat = described_class.new(options)
|
|
163
|
+
expect(query_strat.send(:ordering?)).to eq(false)
|
|
164
|
+
end
|
|
44
165
|
end
|
|
45
166
|
end
|
|
46
167
|
end
|
|
@@ -3,6 +3,8 @@ require 'spec_helper'
|
|
|
3
3
|
# The functionality of this is mainly tested in more integration-y tests in presenter_collection_spec.rb
|
|
4
4
|
|
|
5
5
|
describe Brainstem::QueryStrategies::FilterOrSearch do
|
|
6
|
+
it_behaves_like Brainstem::QueryStrategies::BaseStrategy
|
|
7
|
+
|
|
6
8
|
describe '#execute' do
|
|
7
9
|
context 'we are searching' do
|
|
8
10
|
let(:subject) { described_class.new(@options) }
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
|
|
3
|
+
shared_examples_for Brainstem::QueryStrategies::BaseStrategy do
|
|
4
|
+
let(:strategy) { described_class.new(options) }
|
|
5
|
+
|
|
6
|
+
describe "#calculate_per_page" do
|
|
7
|
+
let(:result) { strategy.calculate_per_page }
|
|
8
|
+
|
|
9
|
+
[
|
|
10
|
+
# per_page default_per_page max_per_page default_max_per_page expected situation used
|
|
11
|
+
[ 10, 20, 100, 200, 10, "per page < max", "the per page"],
|
|
12
|
+
[ nil, 20, 100, 200, 20, "no per page and default < max", "the default"],
|
|
13
|
+
[ nil, 200, 100, 200, 100, "no per page and default > max", "the max"],
|
|
14
|
+
[ 150, 20, 100, 200, 100, "per page > max", "the max"],
|
|
15
|
+
[ 150, 20, nil, 200, 150, "no max and per page < default max", "the per page"],
|
|
16
|
+
[ 250, 20, nil, 200, 200, "no max and per page > default max", "the default max"],
|
|
17
|
+
].each do |per_page, default_per_page, max_per_page, default_max_per_page, expected, situation, used|
|
|
18
|
+
describe "when #{situation}" do
|
|
19
|
+
let(:options) {
|
|
20
|
+
{
|
|
21
|
+
params: {
|
|
22
|
+
per_page: per_page,
|
|
23
|
+
},
|
|
24
|
+
default_per_page: default_per_page,
|
|
25
|
+
default_max_per_page: default_max_per_page,
|
|
26
|
+
max_per_page: max_per_page,
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
it "uses #{used}" do
|
|
31
|
+
expect(result).to eq(expected)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: brainstem
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mavenlink
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2018-01-03 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activerecord
|
|
@@ -164,6 +164,20 @@ dependencies:
|
|
|
164
164
|
- - ">="
|
|
165
165
|
- !ruby/object:Gem::Version
|
|
166
166
|
version: '0'
|
|
167
|
+
- !ruby/object:Gem::Dependency
|
|
168
|
+
name: db-query-matchers
|
|
169
|
+
requirement: !ruby/object:Gem::Requirement
|
|
170
|
+
requirements:
|
|
171
|
+
- - ">="
|
|
172
|
+
- !ruby/object:Gem::Version
|
|
173
|
+
version: '0'
|
|
174
|
+
type: :development
|
|
175
|
+
prerelease: false
|
|
176
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
177
|
+
requirements:
|
|
178
|
+
- - ">="
|
|
179
|
+
- !ruby/object:Gem::Version
|
|
180
|
+
version: '0'
|
|
167
181
|
description: Brainstem allows you to create rich API presenters that know how to filter,
|
|
168
182
|
sort, and include associations.
|
|
169
183
|
email:
|
|
@@ -291,6 +305,7 @@ files:
|
|
|
291
305
|
- spec/brainstem_spec.rb
|
|
292
306
|
- spec/dummy/rails.rb
|
|
293
307
|
- spec/shared/atlas_taker.rb
|
|
308
|
+
- spec/shared/base_strategy.rb
|
|
294
309
|
- spec/shared/formattable.rb
|
|
295
310
|
- spec/spec_helper.rb
|
|
296
311
|
- spec/spec_helpers/db.rb
|
|
@@ -317,7 +332,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
317
332
|
version: '0'
|
|
318
333
|
requirements: []
|
|
319
334
|
rubyforge_project:
|
|
320
|
-
rubygems_version: 2.
|
|
335
|
+
rubygems_version: 2.6.13
|
|
321
336
|
signing_key:
|
|
322
337
|
specification_version: 4
|
|
323
338
|
summary: ActiveRecord presenters with a rich request API
|
|
@@ -368,6 +383,7 @@ test_files:
|
|
|
368
383
|
- spec/brainstem_spec.rb
|
|
369
384
|
- spec/dummy/rails.rb
|
|
370
385
|
- spec/shared/atlas_taker.rb
|
|
386
|
+
- spec/shared/base_strategy.rb
|
|
371
387
|
- spec/shared/formattable.rb
|
|
372
388
|
- spec/spec_helper.rb
|
|
373
389
|
- spec/spec_helpers/db.rb
|