brainstem 0.2.6.1 → 1.0.0.pre.1
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 +5 -13
- data/CHANGELOG.md +16 -2
- data/Gemfile.lock +51 -36
- data/README.md +531 -110
- data/brainstem.gemspec +6 -2
- data/lib/brainstem.rb +25 -9
- data/lib/brainstem/concerns/controller_param_management.rb +22 -0
- data/lib/brainstem/concerns/error_presentation.rb +58 -0
- data/lib/brainstem/concerns/inheritable_configuration.rb +29 -0
- data/lib/brainstem/concerns/lookup.rb +30 -0
- data/lib/brainstem/concerns/presenter_dsl.rb +111 -0
- data/lib/brainstem/controller_methods.rb +17 -8
- data/lib/brainstem/dsl/association.rb +55 -0
- data/lib/brainstem/dsl/associations_block.rb +12 -0
- data/lib/brainstem/dsl/base_block.rb +31 -0
- data/lib/brainstem/dsl/conditional.rb +25 -0
- data/lib/brainstem/dsl/conditionals_block.rb +15 -0
- data/lib/brainstem/dsl/configuration.rb +112 -0
- data/lib/brainstem/dsl/field.rb +68 -0
- data/lib/brainstem/dsl/fields_block.rb +25 -0
- data/lib/brainstem/preloader.rb +98 -0
- data/lib/brainstem/presenter.rb +325 -134
- data/lib/brainstem/presenter_collection.rb +82 -286
- data/lib/brainstem/presenter_validator.rb +96 -0
- data/lib/brainstem/query_strategies/README.md +107 -0
- data/lib/brainstem/query_strategies/base_strategy.rb +62 -0
- data/lib/brainstem/query_strategies/filter_and_search.rb +50 -0
- data/lib/brainstem/query_strategies/filter_or_search.rb +103 -0
- data/lib/brainstem/test_helpers.rb +5 -1
- data/lib/brainstem/version.rb +1 -1
- data/spec/brainstem/concerns/controller_param_management_spec.rb +42 -0
- data/spec/brainstem/concerns/error_presentation_spec.rb +113 -0
- data/spec/brainstem/concerns/inheritable_configuration_spec.rb +210 -0
- data/spec/brainstem/concerns/presenter_dsl_spec.rb +412 -0
- data/spec/brainstem/controller_methods_spec.rb +15 -27
- data/spec/brainstem/dsl/association_spec.rb +123 -0
- data/spec/brainstem/dsl/conditional_spec.rb +93 -0
- data/spec/brainstem/dsl/configuration_spec.rb +1 -0
- data/spec/brainstem/dsl/field_spec.rb +212 -0
- data/spec/brainstem/preloader_spec.rb +137 -0
- data/spec/brainstem/presenter_collection_spec.rb +565 -244
- data/spec/brainstem/presenter_spec.rb +726 -167
- data/spec/brainstem/presenter_validator_spec.rb +209 -0
- data/spec/brainstem/query_strategies/filter_and_search_spec.rb +46 -0
- data/spec/brainstem/query_strategies/filter_or_search_spec.rb +45 -0
- data/spec/spec_helper.rb +11 -3
- data/spec/spec_helpers/db.rb +32 -65
- data/spec/spec_helpers/presenters.rb +124 -29
- data/spec/spec_helpers/rr.rb +11 -0
- data/spec/spec_helpers/schema.rb +115 -0
- metadata +126 -30
- data/lib/brainstem/association_field.rb +0 -53
- data/lib/brainstem/engine.rb +0 -4
- data/pkg/brainstem-0.2.5.gem +0 -0
- data/pkg/brainstem-0.2.6.gem +0 -0
- data/spec/spec_helpers/cleanup.rb +0 -23
@@ -1,30 +1,7 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe Brainstem::Presenter do
|
4
|
-
describe "class
|
5
|
-
|
6
|
-
describe "presents method" do
|
7
|
-
before do
|
8
|
-
@klass = Class.new(Brainstem::Presenter)
|
9
|
-
end
|
10
|
-
|
11
|
-
it "records itself as the presenter for the named class as a string" do
|
12
|
-
@klass.presents "String"
|
13
|
-
expect(Brainstem.presenter_collection.for(String)).to be_a(@klass)
|
14
|
-
end
|
15
|
-
|
16
|
-
it "records itself as the presenter for the given class" do
|
17
|
-
@klass.presents String
|
18
|
-
expect(Brainstem.presenter_collection.for(String)).to be_a(@klass)
|
19
|
-
end
|
20
|
-
|
21
|
-
it "records itself as the presenter for the named classes" do
|
22
|
-
@klass.presents String, Array
|
23
|
-
expect(Brainstem.presenter_collection.for(String)).to be_a(@klass)
|
24
|
-
expect(Brainstem.presenter_collection.for(Array)).to be_a(@klass)
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
4
|
+
describe "class behavior" do
|
28
5
|
describe "implicit namespacing" do
|
29
6
|
module V1
|
30
7
|
class SomePresenter < Brainstem::Presenter
|
@@ -33,7 +10,8 @@ describe Brainstem::Presenter do
|
|
33
10
|
|
34
11
|
it "uses the closest module name as the presenter namespace" do
|
35
12
|
V1::SomePresenter.presents String
|
36
|
-
expect(Brainstem.presenter_collection(
|
13
|
+
expect(Brainstem.presenter_collection('v1').for(String)).to be_a(V1::SomePresenter)
|
14
|
+
expect(V1::SomePresenter.namespace).to eq 'v1'
|
37
15
|
end
|
38
16
|
|
39
17
|
it "does not map namespaced presenters into the default namespace" do
|
@@ -42,67 +20,264 @@ describe Brainstem::Presenter do
|
|
42
20
|
end
|
43
21
|
end
|
44
22
|
|
45
|
-
describe
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
23
|
+
describe '.presents' do
|
24
|
+
let!(:presenter_class) { Class.new(Brainstem::Presenter) }
|
25
|
+
|
26
|
+
it 'records itself as the presenter for the given class' do
|
27
|
+
presenter_class.presents String
|
28
|
+
expect(Brainstem.presenter_collection.for(String)).to be_a(presenter_class)
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'records itself as the presenter for the given classes' do
|
32
|
+
presenter_class.presents String, Array
|
33
|
+
expect(Brainstem.presenter_collection.for(String)).to be_a(presenter_class)
|
34
|
+
expect(Brainstem.presenter_collection.for(Array)).to be_a(presenter_class)
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'can be called more than once' do
|
38
|
+
presenter_class.presents String
|
39
|
+
presenter_class.presents Array
|
40
|
+
expect(Brainstem.presenter_collection.for(String)).to be_a(presenter_class)
|
41
|
+
expect(Brainstem.presenter_collection.for(Array)).to be_a(presenter_class)
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'returns the set of presented classes' do
|
45
|
+
expect(presenter_class.presents(String)).to eq([String])
|
46
|
+
expect(presenter_class.presents(Array)).to eq([String, Array])
|
47
|
+
expect(presenter_class.presents).to eq([String, Array])
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'removes duplicates when called more then once' do
|
51
|
+
expect(presenter_class.presents(Array)).to eq([Array])
|
52
|
+
expect(presenter_class.presents(Array, Array)).to eq([Array])
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'should not be inherited' do
|
56
|
+
presenter_class.presents(String)
|
57
|
+
expect(presenter_class.presents).to eq [String]
|
58
|
+
subclass = Class.new(presenter_class)
|
59
|
+
expect(subclass.presents).to eq []
|
60
|
+
subclass.presents(Array)
|
61
|
+
expect(subclass.presents).to eq [Array]
|
62
|
+
expect(presenter_class.presents).to eq [String]
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'raises an error when given a string' do
|
66
|
+
expect(lambda {
|
67
|
+
presenter_class.presents 'Array'
|
68
|
+
}).to raise_error(/Brainstem Presenter#presents now expects a Class instead of a class name/)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
describe "#group_present" do
|
74
|
+
let(:presenter_class) do
|
75
|
+
Class.new(Brainstem::Presenter) do
|
76
|
+
presents Workspace
|
77
|
+
|
78
|
+
helper do
|
79
|
+
def helper_method(model)
|
80
|
+
Task.where(:workspace_id => model.id)[0..1]
|
50
81
|
end
|
51
82
|
end
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
end
|
83
|
+
|
84
|
+
fields do
|
85
|
+
field :updated_at, :datetime
|
56
86
|
end
|
57
|
-
end
|
58
87
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
88
|
+
associations do
|
89
|
+
association :tasks, Task
|
90
|
+
association :user, User
|
91
|
+
association :missing_user, User
|
92
|
+
association :something, User, via: :user
|
93
|
+
association :lead_user, User
|
94
|
+
association :lead_user_with_lambda, User, dynamic: lambda { |model| model.user }
|
95
|
+
association :tasks_with_lambda, Task, dynamic: lambda { |model| Task.where(:workspace_id => model.id) }
|
96
|
+
association :tasks_with_helper_lambda, Task, dynamic: lambda { |model| helper_method(model) }
|
97
|
+
association :tasks_with_lookup, Task, lookup: lambda { |models| Task.where(workspace_id: models.map(&:id)).group_by { |task| task.workspace_id } }
|
98
|
+
association :tasks_with_lookup_fetch, Task,
|
99
|
+
lookup: lambda { |models| Task.where(workspace_id: models.map(&:id)).group_by { |task| task.workspace_id } },
|
100
|
+
lookup_fetch: lambda { |lookup, model| lookup[model.id] }
|
101
|
+
association :synthetic, :polymorphic
|
102
|
+
end
|
64
103
|
end
|
65
104
|
end
|
105
|
+
|
106
|
+
let(:workspace) { Workspace.find_by_title("bob workspace 1") }
|
107
|
+
let(:presenter) { presenter_class.new }
|
66
108
|
|
67
|
-
describe "
|
68
|
-
|
69
|
-
|
109
|
+
describe "the field DSL" do
|
110
|
+
let(:presenter_class) { Class.new(WorkspacePresenter) }
|
111
|
+
let(:presenter) { presenter_class.new }
|
112
|
+
let(:model) { Workspace.find(1) }
|
113
|
+
|
114
|
+
it 'calls named methods' do
|
115
|
+
expect(presenter.group_present([model]).first['title']).to eq model.title
|
70
116
|
end
|
71
117
|
|
72
|
-
it
|
73
|
-
|
74
|
-
expect(
|
75
|
-
expect(@klass.filters[:foo][1]).to be_a(Proc)
|
118
|
+
it 'can call methods with :via' do
|
119
|
+
presenter.configuration[:fields][:title].options[:via] = :description
|
120
|
+
expect(presenter.group_present([model]).first['title']).to eq model.description
|
76
121
|
end
|
77
122
|
|
78
|
-
it
|
79
|
-
|
80
|
-
expect(@klass.filters[:foo][1]).to be_nil
|
123
|
+
it 'can call a dynamic lambda' do
|
124
|
+
expect(presenter.group_present([model]).first['dynamic_title']).to eq "title: #{model.title}"
|
81
125
|
end
|
82
|
-
end
|
83
126
|
|
84
|
-
|
85
|
-
|
86
|
-
|
127
|
+
it 'can call a lookup lambda' do
|
128
|
+
expect(presenter.group_present([model]).first['lookup_title']).to eq "lookup_title: #{model.title}"
|
129
|
+
end
|
130
|
+
|
131
|
+
it 'can call a lookup_fetch lambda' do
|
132
|
+
expect(presenter.group_present([model]).first['lookup_fetch_title']).to eq "lookup_fetch_title: #{model.title}"
|
87
133
|
end
|
88
134
|
|
89
|
-
it
|
90
|
-
|
91
|
-
|
135
|
+
it 'handles nesting' do
|
136
|
+
expect(presenter.group_present([model]).first['permissions']['access_level']).to eq 2
|
137
|
+
end
|
138
|
+
|
139
|
+
describe 'handling of conditional fields' do
|
140
|
+
it 'does not return conditional fields when their :if conditionals do not match' do
|
141
|
+
expect(presenter.group_present([model]).first['secret']).to be_nil
|
142
|
+
expect(presenter.group_present([model]).first['bob_title']).to be_nil
|
143
|
+
end
|
144
|
+
|
145
|
+
it 'returns conditional fields when their :if matches' do
|
146
|
+
model.title = 'hello'
|
147
|
+
expect(presenter.group_present([model]).first['hello_title']).to eq 'title is hello'
|
148
|
+
end
|
149
|
+
|
150
|
+
it 'returns fields with the :if option only when all of the conditionals in that :if are true' do
|
151
|
+
model.title = 'hello'
|
152
|
+
presenter.class.helper do
|
153
|
+
def current_user
|
154
|
+
'not bob'
|
155
|
+
end
|
156
|
+
end
|
157
|
+
expect(presenter.group_present([model]).first['secret']).to be_nil
|
158
|
+
presenter.class.helper do
|
159
|
+
def current_user
|
160
|
+
'bob'
|
161
|
+
end
|
162
|
+
end
|
163
|
+
expect(presenter.group_present([model]).first['secret']).to eq model.secret_info
|
164
|
+
end
|
165
|
+
|
166
|
+
describe "caching of conditional evaluations" do
|
167
|
+
it 'only runs model conditionals once per model' do
|
168
|
+
model_id_call_count = { 1 => 0, 2 => 0 }
|
169
|
+
|
170
|
+
presenter_class.conditionals do
|
171
|
+
model :model_id_is_two, lambda { |model| model_id_call_count[model.id] += 1; model.id == 2 }
|
172
|
+
end
|
173
|
+
|
174
|
+
presenter_class.fields do
|
175
|
+
field :only_on_model_two, :string,
|
176
|
+
dynamic: lambda { "some value" },
|
177
|
+
if: :model_id_is_two
|
178
|
+
field :another_only_on_model_two, :string,
|
179
|
+
dynamic: lambda { "some value" },
|
180
|
+
if: :model_id_is_two
|
181
|
+
end
|
182
|
+
|
183
|
+
results = presenter.group_present([Workspace.find(1), Workspace.find(2)])
|
184
|
+
expect(results.first['only_on_model_two']).not_to be_present
|
185
|
+
expect(results.last['only_on_model_two']).to be_present
|
186
|
+
expect(results.first['another_only_on_model_two']).not_to be_present
|
187
|
+
expect(results.last['another_only_on_model_two']).to be_present
|
188
|
+
expect(results.first['id']).to eq '1'
|
189
|
+
expect(results.last['id']).to eq '2'
|
190
|
+
|
191
|
+
expect(model_id_call_count).to eq({ 1 => 1, 2 => 1 })
|
192
|
+
end
|
193
|
+
|
194
|
+
it 'only runs request conditionals once per request' do
|
195
|
+
call_count = 0
|
196
|
+
|
197
|
+
presenter_class.conditionals do
|
198
|
+
request :new_request_conditional, lambda { call_count += 1 }
|
199
|
+
end
|
200
|
+
|
201
|
+
presenter_class.fields do
|
202
|
+
field :new_field, :string,
|
203
|
+
dynamic: lambda { "new_field value" },
|
204
|
+
if: :new_request_conditional
|
205
|
+
field :new_field2, :string,
|
206
|
+
dynamic: lambda { "new_field2 value" },
|
207
|
+
if: :new_request_conditional
|
208
|
+
end
|
209
|
+
|
210
|
+
presenter.group_present([Workspace.find(1), Workspace.find(2)])
|
211
|
+
|
212
|
+
expect(call_count).to eq 1
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
describe 'helpers in dynamic fields' do
|
218
|
+
let(:presenter_class) do
|
219
|
+
Class.new(Brainstem::Presenter) do
|
220
|
+
helper do
|
221
|
+
def counter
|
222
|
+
@count ||= 0
|
223
|
+
@count += 1
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
fields do
|
228
|
+
field :memoized_helper_value1, :integer, dynamic: lambda { |model| counter }
|
229
|
+
field :memoized_helper_value2, :integer, dynamic: lambda { |model| counter }
|
230
|
+
field :memoized_helper_value3, :integer, dynamic: lambda { |model| counter }
|
231
|
+
field :memoized_helper_value4, :integer, dynamic: lambda { |model| counter }
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
let(:presenter) { presenter_class.new }
|
237
|
+
|
238
|
+
it 'shares the helper instance across fields, but not across instances' do
|
239
|
+
fields = presenter.group_present([model, model])
|
240
|
+
expect(fields[0].slice(*%w[memoized_helper_value1 memoized_helper_value2 memoized_helper_value3 memoized_helper_value4]).values).to match_array [1, 2, 3, 4]
|
241
|
+
expect(fields[1].slice(*%w[memoized_helper_value1 memoized_helper_value2 memoized_helper_value3 memoized_helper_value4]).values).to match_array [1, 2, 3, 4]
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
describe 'handling of optional fields' do
|
246
|
+
it 'does not include optional fields by default' do
|
247
|
+
expect(presenter.group_present([model]).first).not_to have_key('expensive_title')
|
248
|
+
end
|
249
|
+
|
250
|
+
it 'includes optional fields when explicitly requested' do
|
251
|
+
presented_workspace = presenter.group_present([model], [], optional_fields: ['expensive_title', 'expensive_title2']).first
|
252
|
+
|
253
|
+
expect(presented_workspace).to have_key('expensive_title')
|
254
|
+
expect(presented_workspace).to have_key('expensive_title2')
|
255
|
+
expect(presented_workspace).not_to have_key('expensive_title3')
|
256
|
+
end
|
257
|
+
|
258
|
+
context 'handling of conditional' do
|
259
|
+
it 'does not include field when condition is not met' do
|
260
|
+
model.title = 'Not hello'
|
261
|
+
presented_workspace = presenter.group_present([model], [], optional_fields: ['conditional_expensive_title']).first
|
262
|
+
expect(presented_workspace).not_to have_key('conditional_expensive_title')
|
263
|
+
end
|
264
|
+
|
265
|
+
it 'includes field when condition is met' do
|
266
|
+
model.title = 'hello'
|
267
|
+
presented_workspace = presenter.group_present([model], [], optional_fields: ['conditional_expensive_title']).first
|
268
|
+
expect(presented_workspace).to have_key('conditional_expensive_title')
|
269
|
+
end
|
270
|
+
end
|
92
271
|
end
|
93
272
|
end
|
94
|
-
end
|
95
273
|
|
96
|
-
describe "post_process hooks" do
|
97
274
|
describe "adding object ids as strings" do
|
98
275
|
before do
|
99
276
|
post_presenter = Class.new(Brainstem::Presenter) do
|
100
277
|
presents Post
|
101
278
|
|
102
|
-
|
103
|
-
|
104
|
-
:body => model.body,
|
105
|
-
}
|
279
|
+
fields do
|
280
|
+
field :body, :string
|
106
281
|
end
|
107
282
|
end
|
108
283
|
|
@@ -111,167 +286,551 @@ describe Brainstem::Presenter do
|
|
111
286
|
end
|
112
287
|
|
113
288
|
it "outputs the associated object's id and type" do
|
114
|
-
data = @presenter.
|
115
|
-
expect(data[
|
116
|
-
expect(data[
|
289
|
+
data = @presenter.group_present([@post]).first
|
290
|
+
expect(data['id']).to eq(@post.id.to_s)
|
291
|
+
expect(data['body']).to eq(@post.body)
|
117
292
|
end
|
118
293
|
end
|
119
294
|
|
120
295
|
describe "converting dates and times" do
|
121
296
|
it "should convert all Time-and-date-like objects to iso8601" do
|
122
|
-
|
123
|
-
|
124
|
-
{
|
125
|
-
|
126
|
-
|
127
|
-
:
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
}
|
132
|
-
}
|
297
|
+
presenter = Class.new(Brainstem::Presenter) do
|
298
|
+
fields do
|
299
|
+
field :time, :datetime, dynamic: lambda { Time.now }
|
300
|
+
field :date, :date, dynamic: lambda { Date.new }
|
301
|
+
fields :recursion do
|
302
|
+
field :time, :datetime, dynamic: lambda { Time.now }
|
303
|
+
field :something, :datetime, dynamic: lambda { [Time.now, :else] }
|
304
|
+
field :foo, :string, dynamic: lambda { :bar }
|
305
|
+
end
|
133
306
|
end
|
134
307
|
end
|
135
308
|
|
136
309
|
iso8601_time = /\d{4}-\d{2}-\d{2}T\d{2}\:\d{2}\:\d{2}[-+]\d{2}:\d{2}/
|
137
310
|
iso8601_date = /\d{4}-\d{2}-\d{2}/
|
138
311
|
|
139
|
-
struct =
|
140
|
-
expect(struct[
|
141
|
-
expect(struct[
|
142
|
-
expect(struct[
|
143
|
-
expect(struct[
|
144
|
-
expect(struct[
|
145
|
-
expect(struct[
|
312
|
+
struct = presenter.new.group_present([Workspace.first]).first
|
313
|
+
expect(struct['time']).to match(iso8601_time)
|
314
|
+
expect(struct['date']).to match(iso8601_date)
|
315
|
+
expect(struct['recursion']['time']).to match(iso8601_time)
|
316
|
+
expect(struct['recursion']['something'].first).to match(iso8601_time)
|
317
|
+
expect(struct['recursion']['something'].last).to eq(:else)
|
318
|
+
expect(struct['recursion']['foo']).to eq(:bar)
|
146
319
|
end
|
147
320
|
end
|
148
321
|
|
149
|
-
describe "outputting
|
150
|
-
|
151
|
-
|
152
|
-
|
322
|
+
describe "outputting associations" do
|
323
|
+
it "should not convert or return non-included associations, but should return <association>_id for belongs_to relationships, plus all fields" do
|
324
|
+
json = presenter.group_present([workspace], []).first
|
325
|
+
expect(json.keys).to match_array %w[id updated_at something_id user_id]
|
326
|
+
end
|
153
327
|
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
328
|
+
it "should convert requested has_many associations (includes) into the <association>_ids format" do
|
329
|
+
expect(workspace.tasks.length).to be > 0
|
330
|
+
expect(presenter.group_present([workspace], ['tasks']).first['task_ids']).to match_array(workspace.tasks.map(&:id).map(&:to_s))
|
331
|
+
end
|
332
|
+
|
333
|
+
it "should ignore unknown associations" do
|
334
|
+
result = presenter.group_present(Workspace.all.to_a, ['tasks', 'unknown'])
|
335
|
+
expect(result.length).to eq Workspace.count
|
336
|
+
expect(result.last['task_ids']).to eq Workspace.last.tasks.pluck(:id).map(&:to_s)
|
337
|
+
end
|
338
|
+
|
339
|
+
it "should allow has_many associations to work on groups of models" do
|
340
|
+
result = presenter.group_present(Workspace.all.to_a, ['tasks'])
|
341
|
+
expect(result.length).to eq Workspace.count
|
342
|
+
first_workspace_tasks = Workspace.first.tasks.pluck(:id).map(&:to_s)
|
343
|
+
last_workspace_tasks = Workspace.last.tasks.pluck(:id).map(&:to_s)
|
344
|
+
expect(last_workspace_tasks.length).to eq 1
|
345
|
+
expect(result.last['task_ids']).to eq last_workspace_tasks
|
346
|
+
expect(result.first['task_ids']).to eq first_workspace_tasks
|
347
|
+
end
|
348
|
+
|
349
|
+
it "should convert requested belongs_to and has_one associations into the <association>_id format when requested" do
|
350
|
+
expect(presenter.group_present([workspace], ['user']).first['user_id']).to eq(workspace.user.id.to_s)
|
351
|
+
end
|
352
|
+
|
353
|
+
it "should convert requested belongs_to and has_one associations into the <association>_id format when requested, even if they're not found" do
|
354
|
+
expect(presenter.group_present([workspace], ['missing_user']).first).to have_key('missing_user_id')
|
355
|
+
expect(presenter.group_present([workspace], ['missing_user']).first['missing_user_id']).to eq(nil)
|
356
|
+
end
|
357
|
+
|
358
|
+
it "converts non-association models into <model>_id format when they are requested" do
|
359
|
+
expect(presenter.group_present([workspace], ['lead_user']).first['lead_user_id']).to eq(workspace.lead_user.id.to_s)
|
360
|
+
end
|
361
|
+
|
362
|
+
it "handles associations provided with lambdas" do
|
363
|
+
expect(presenter.group_present([workspace], ['lead_user_with_lambda']).first['lead_user_with_lambda_id']).to eq(workspace.lead_user.id.to_s)
|
364
|
+
expect(presenter.group_present([workspace], ['tasks_with_lambda']).first['tasks_with_lambda_ids']).to eq(workspace.tasks.map(&:id).map(&:to_s))
|
365
|
+
end
|
366
|
+
|
367
|
+
it "handles associations provided with a lookup" do
|
368
|
+
expect(presenter.group_present([workspace], ['tasks_with_lookup']).first['tasks_with_lookup_ids']).to eq(workspace.tasks.map(&:id).map(&:to_s))
|
369
|
+
end
|
370
|
+
|
371
|
+
it "handles associations provided with a lookup_fetch" do
|
372
|
+
expect(presenter.group_present([workspace], ['tasks_with_lookup_fetch']).first['tasks_with_lookup_fetch_ids']).to eq(workspace.tasks.map(&:id).map(&:to_s))
|
373
|
+
end
|
163
374
|
|
164
|
-
|
375
|
+
it "handles helpers method calls in association lambdas" do
|
376
|
+
expect(presenter.group_present([workspace], ['tasks_with_helper_lambda']).first['tasks_with_helper_lambda_ids']).to eq(workspace.tasks.map(&:id).map(&:to_s)[0..1])
|
165
377
|
end
|
166
378
|
|
167
|
-
|
379
|
+
it "should return <association>_id fields when the given association ids exist on the model whether it is requested or not" do
|
380
|
+
expect(presenter.group_present([workspace], ['user']).first['user_id']).to eq(workspace.user_id.to_s)
|
168
381
|
|
169
|
-
|
170
|
-
|
382
|
+
json = presenter.group_present([workspace], []).first
|
383
|
+
expect(json.keys).to match_array %w[user_id something_id id updated_at]
|
384
|
+
expect(json['user_id']).to eq(workspace.user_id.to_s)
|
385
|
+
expect(json['something_id']).to eq(workspace.user_id.to_s)
|
386
|
+
end
|
171
387
|
|
388
|
+
it "should return null, not empty string when ids are missing" do
|
389
|
+
workspace.user = nil
|
390
|
+
workspace.tasks = []
|
391
|
+
expect(presenter.group_present([workspace], ['lead_user_with_lambda']).first['lead_user_with_lambda_id']).to eq(nil)
|
392
|
+
expect(presenter.group_present([workspace], ['user']).first['user_id']).to eq(nil)
|
393
|
+
expect(presenter.group_present([workspace], ['something']).first['something_id']).to eq(nil)
|
394
|
+
expect(presenter.group_present([workspace], ['tasks']).first['task_ids']).to eq([])
|
395
|
+
end
|
172
396
|
|
173
|
-
|
174
|
-
|
175
|
-
|
397
|
+
describe "polymorphic associations" do
|
398
|
+
let(:some_presenter) do
|
399
|
+
Class.new(Brainstem::Presenter) do
|
400
|
+
presents Post
|
401
|
+
|
402
|
+
fields do
|
403
|
+
field :body, :string
|
404
|
+
end
|
405
|
+
|
406
|
+
associations do
|
407
|
+
association :subject, :polymorphic
|
408
|
+
association :another_subject, :polymorphic, via: :subject
|
409
|
+
association :forced_model, Workspace, via: :subject
|
410
|
+
association :things, :polymorphic
|
411
|
+
end
|
412
|
+
end
|
176
413
|
end
|
177
414
|
|
178
|
-
|
179
|
-
|
180
|
-
|
415
|
+
let(:presenter) { some_presenter.new }
|
416
|
+
|
417
|
+
context "when asking for the association" do
|
418
|
+
let(:presented_data) { presenter.group_present([post], %w[subject another_subject forced_model things]).first }
|
419
|
+
|
420
|
+
context "when polymorphic association exists" do
|
421
|
+
let(:post) { Post.find(1) }
|
422
|
+
|
423
|
+
it "outputs the object as a hash with the id & class table name" do
|
424
|
+
expect(presented_data['subject_ref']).to eq({ 'id' => post.subject.id.to_s,
|
425
|
+
'key' => 'workspaces' })
|
426
|
+
end
|
427
|
+
|
428
|
+
it "outputs custom names for the object as a hash with the id & class table name" do
|
429
|
+
expect(presented_data['another_subject_ref']).to eq({ 'id' => post.subject.id.to_s,
|
430
|
+
'key' => 'workspaces' })
|
431
|
+
end
|
432
|
+
|
433
|
+
context "presenting a mixture of things" do
|
434
|
+
it 'will return a *_refs array' do
|
435
|
+
expect(presented_data['thing_refs']).to eq [
|
436
|
+
{ 'id' => '1', 'key' => 'workspaces' },
|
437
|
+
{ 'id' => '1', 'key' => 'posts' },
|
438
|
+
{ 'id' => '1', 'key' => 'tasks' }
|
439
|
+
]
|
440
|
+
end
|
441
|
+
end
|
442
|
+
|
443
|
+
context "for STI targets" do
|
444
|
+
let(:post) { Post.create!(subject: Attachments::PostAttachment.first, user: User.first, body: '1 2 3') }
|
445
|
+
|
446
|
+
it "uses the brainstem_key from the presenter" do
|
447
|
+
expect(presented_data['subject_ref']).to eq({ 'id' => post.subject_id.to_s,
|
448
|
+
'key' => 'attachments' })
|
449
|
+
end
|
450
|
+
|
451
|
+
it "uses the correct namespace when finding a presenter" do
|
452
|
+
module V2
|
453
|
+
class NewPostPresenter < Brainstem::Presenter
|
454
|
+
presents Post
|
455
|
+
|
456
|
+
associations do
|
457
|
+
association :subject, :polymorphic
|
458
|
+
end
|
459
|
+
end
|
460
|
+
end
|
461
|
+
|
462
|
+
expect {
|
463
|
+
V2::NewPostPresenter.new.group_present([post], %w[subject]).first
|
464
|
+
}.to raise_error(/Unable to find a presenter for class Attachments::PostAttachment/)
|
465
|
+
end
|
466
|
+
end
|
467
|
+
|
468
|
+
it "skips the polymorphic handling when a model is given" do
|
469
|
+
expect(presented_data['forced_model_id']).to eq(post.subject.id.to_s)
|
470
|
+
expect(presented_data).not_to have_key('forced_model_type')
|
471
|
+
expect(presented_data).not_to have_key('forced_model_ref')
|
472
|
+
end
|
473
|
+
|
474
|
+
describe "the legacy :always_return_ref_with_sti_base option" do
|
475
|
+
before do
|
476
|
+
some_presenter.associations do
|
477
|
+
association :always_subject, :polymorphic, via: :subject,
|
478
|
+
always_return_ref_with_sti_base: true
|
479
|
+
end
|
480
|
+
end
|
481
|
+
|
482
|
+
let(:post) { Post.create!(subject: Attachments::PostAttachment.first, user: User.first, body: '1 2 3') }
|
483
|
+
|
484
|
+
describe 'when the presenter can be found' do
|
485
|
+
before do
|
486
|
+
Class.new(Brainstem::Presenter) do
|
487
|
+
presents Attachments::Base
|
488
|
+
|
489
|
+
brainstem_key :foo
|
490
|
+
end
|
491
|
+
end
|
492
|
+
|
493
|
+
it "always returns the *_ref object, even when not included" do
|
494
|
+
expect(presented_data['always_subject_ref']).to eq({ 'id' => post.subject.id.to_s,
|
495
|
+
'key' => 'foo' })
|
496
|
+
end
|
497
|
+
end
|
498
|
+
|
499
|
+
# It tries to find the key based on the *_type value in the DB (which will be the STI base class, and may error if no presenter exists)
|
500
|
+
describe 'when the presenter cannot be found' do
|
501
|
+
it "raises an error" do
|
502
|
+
expect { presented_data['always_subject_ref'] }.to raise_error(/Unable to find a presenter for class Attachments::Base/)
|
503
|
+
end
|
504
|
+
end
|
505
|
+
end
|
506
|
+
end
|
507
|
+
|
508
|
+
context "when polymorphic association does not exist" do
|
509
|
+
let(:post) { Post.find(3) }
|
510
|
+
|
511
|
+
it "outputs nil" do
|
512
|
+
expect(presented_data).to have_key('subject_ref')
|
513
|
+
expect(presented_data['subject_ref']).to be_nil
|
514
|
+
end
|
515
|
+
|
516
|
+
it "outputs nil" do
|
517
|
+
expect(presented_data).to have_key('another_subject_ref')
|
518
|
+
expect(presented_data['another_subject_ref']).to be_nil
|
519
|
+
end
|
520
|
+
end
|
181
521
|
end
|
182
522
|
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
523
|
+
context "when not asking for the association" do
|
524
|
+
let(:presented_data) { presenter.group_present([post]).first }
|
525
|
+
let(:post) { Post.find(1) }
|
526
|
+
|
527
|
+
it "does not include the reference" do
|
528
|
+
expect(presented_data).to_not have_key('subject_ref')
|
529
|
+
expect(presented_data).to_not have_key('another_subject_ref')
|
530
|
+
end
|
187
531
|
end
|
188
532
|
end
|
189
533
|
|
190
|
-
context "when
|
191
|
-
|
534
|
+
context "when the model has an <association>_id method but no column" do
|
535
|
+
it "does not include the <association>_id field" do
|
536
|
+
def workspace.synthetic_id
|
537
|
+
raise "this explodes because it's not an association"
|
538
|
+
end
|
539
|
+
expect(presenter.group_present([workspace], []).first).not_to have_key('synthetic_id')
|
540
|
+
end
|
541
|
+
end
|
542
|
+
end
|
192
543
|
|
193
|
-
|
194
|
-
|
544
|
+
describe "preloading" do
|
545
|
+
|
546
|
+
#
|
547
|
+
# We have three strategies for introspecting what the AR Preloader
|
548
|
+
# receives:
|
549
|
+
#
|
550
|
+
# 1. Actually looking at what AR receives.
|
551
|
+
#
|
552
|
+
# This is sub-optimal because AR works differently between versions
|
553
|
+
# 3 and 4, so introspecting is difficult.
|
554
|
+
#
|
555
|
+
# 2. Looking at what the proc that encapsulates the AR methods is
|
556
|
+
# called with.
|
557
|
+
#
|
558
|
+
# This is difficult to do because we don't control the instantiation
|
559
|
+
# of the Preloader, which makes injecting this hard.
|
560
|
+
#
|
561
|
+
# 3. Intercept the instantiating parent function and append the
|
562
|
+
# inspectable proc.
|
563
|
+
#
|
564
|
+
# This is probably the grossest of all the above options, and I
|
565
|
+
# suspect it's the greatest indication that we're testing
|
566
|
+
# inappropriately here -- a fact that I think bears weight given
|
567
|
+
# we're making unit-level assertions in an integration spec.
|
568
|
+
# However, there's also some value in asserting that these are
|
569
|
+
# passed through without digging into Rails internals. However,
|
570
|
+
# further 'purity' improvements could be made by introspecting on
|
571
|
+
# AR's actual data structures.
|
572
|
+
#
|
573
|
+
# That's about three shades on the side of overkill, though.
|
574
|
+
#
|
575
|
+
def preloader_should_receive(hsh)
|
576
|
+
preload_method = Object.new
|
577
|
+
mock(preload_method).call(anything, anything) do |models, args|
|
578
|
+
expect(args).to eq(hsh)
|
195
579
|
end
|
196
580
|
|
197
|
-
|
198
|
-
|
581
|
+
stub(Brainstem::Preloader).preload(anything, anything, anything) do |*args|
|
582
|
+
args << preload_method
|
583
|
+
Brainstem::Preloader.new(*args).call
|
199
584
|
end
|
200
585
|
end
|
586
|
+
|
587
|
+
|
588
|
+
it "preloads associations when they are full model-level associations" do
|
589
|
+
preloader_should_receive("tasks" => [], "user" => [])
|
590
|
+
presenter.group_present(Workspace.order('id desc'), %w[tasks user lead_user tasks_with_lambda])
|
591
|
+
end
|
592
|
+
|
593
|
+
it "includes any associations declared via the preload DSL directive" do
|
594
|
+
preloader_should_receive("tasks" => [], "posts" => [])
|
595
|
+
presenter_class.preload :posts
|
596
|
+
|
597
|
+
presenter.group_present(Workspace.order('id desc'), %w[tasks lead_user tasks_with_lambda])
|
598
|
+
end
|
599
|
+
|
600
|
+
it "includes any string associations declared via the preload DSL directive" do
|
601
|
+
preloader_should_receive("tasks" => [], "user" => [])
|
602
|
+
presenter_class.preload 'user'
|
603
|
+
|
604
|
+
presenter.group_present(Workspace.order('id desc'), %w[tasks user lead_user tasks_with_lambda])
|
605
|
+
end
|
606
|
+
|
607
|
+
it "includes any nested hash associations declared via the preload DSL directive" do
|
608
|
+
preloader_should_receive("tasks" => [], "user" => [:workspaces], "posts" => ["subject", "user"])
|
609
|
+
presenter_class.preload :tasks, "user", "unknown", { "posts" => "subject", "foo" => "bar" },{ :user => :workspaces, "posts" => "user" }
|
610
|
+
|
611
|
+
presenter.group_present(Workspace.order('id desc'), %w[tasks user lead_user tasks_with_lambda])
|
612
|
+
end
|
201
613
|
end
|
614
|
+
end
|
202
615
|
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
616
|
+
describe "#extract_filters" do
|
617
|
+
let(:presenter_class) { WorkspacePresenter }
|
618
|
+
let(:presenter) { presenter_class.new }
|
619
|
+
|
620
|
+
it 'returns only known filters' do
|
621
|
+
presenter_class.filter :owned_by
|
622
|
+
presenter_class.filter(:bar) { |scope| scope }
|
623
|
+
expect(presenter.extract_filters({ 'foo' => 'hi' })).to eq({})
|
624
|
+
expect(presenter.extract_filters({ 'owned_by' => '2' })).to eq({ 'owned_by' => '2' })
|
625
|
+
expect(presenter.extract_filters({ 'owned_by' => [2] })).to eq({ 'owned_by' => [2] })
|
626
|
+
expect(presenter.extract_filters({ 'owned_by' => { :ids => [2], 2 => [1] }})).to eq({ 'owned_by' => { :ids => [2], 2 => [1] }})
|
627
|
+
end
|
628
|
+
|
629
|
+
it "converts 'true' and 'false' into true and false" do
|
630
|
+
presenter_class.filter :owned_by
|
631
|
+
expect(presenter.extract_filters({ 'owned_by' => 'true' })).to eq({ 'owned_by' => true })
|
632
|
+
expect(presenter.extract_filters({ 'owned_by' => 'TRUE' })).to eq({ 'owned_by' => true })
|
633
|
+
expect(presenter.extract_filters({ 'owned_by' => 'false' })).to eq({ 'owned_by' => false })
|
634
|
+
expect(presenter.extract_filters({ 'owned_by' => 'FALSE' })).to eq({ 'owned_by' => false })
|
635
|
+
expect(presenter.extract_filters({ 'owned_by' => 'hi' })).to eq({ 'owned_by' => 'hi' })
|
636
|
+
end
|
637
|
+
|
638
|
+
it 'defaults to applying default filters' do
|
639
|
+
presenter_class.filter :owned_by, default: '2'
|
640
|
+
expect(presenter.extract_filters({ 'owned_by' => '3' })).to eq({ 'owned_by' => '3' })
|
641
|
+
expect(presenter.extract_filters({})).to eq({ 'owned_by' => '2' })
|
642
|
+
end
|
643
|
+
|
644
|
+
it 'will skip default filters when asked' do
|
645
|
+
presenter_class.filter :owned_by, default: '2'
|
646
|
+
expect(presenter.extract_filters({ 'owned_by' => '3' }, apply_default_filters: false)).to eq({ 'owned_by' => '3' })
|
647
|
+
expect(presenter.extract_filters({}, apply_default_filters: false)).to eq({})
|
648
|
+
end
|
649
|
+
|
650
|
+
it 'ignores nil and blank values' do
|
651
|
+
presenter_class.filter :owned_by
|
652
|
+
expect(presenter.extract_filters({ 'owned_by' => nil })).to eq({})
|
653
|
+
expect(presenter.extract_filters({ 'owned_by' => '' })).to eq({})
|
654
|
+
end
|
655
|
+
end
|
656
|
+
|
657
|
+
describe "#apply_filters_to_scope" do
|
658
|
+
let(:presenter_class) { WorkspacePresenter }
|
659
|
+
let(:presenter) { presenter_class.new }
|
660
|
+
let(:scope) { Workspace.where(nil) }
|
661
|
+
let(:params) { { 'bar' => 'foo' } }
|
662
|
+
let(:options) { { apply_default_filters: true } }
|
663
|
+
|
664
|
+
before do
|
665
|
+
presenter_class.filter :owned_by, default: '2'
|
666
|
+
presenter_class.filter(:bar) { |scope| scope.where(id: 6) }
|
667
|
+
mock(presenter).extract_filters(params, options) { { 'bar' => 'foo', 'owned_by' => '2' } }
|
668
|
+
end
|
669
|
+
|
670
|
+
it 'extracts valid filters from the params' do
|
671
|
+
presenter.apply_filters_to_scope(scope, params, options)
|
672
|
+
end
|
673
|
+
|
674
|
+
it 'runs lambdas in the scope of the helper instance' do
|
675
|
+
expect(presenter.apply_filters_to_scope(scope, params, options).to_sql).to match(/id.\s*=\s*6/)
|
676
|
+
end
|
677
|
+
|
678
|
+
it 'sends symbols to the scope' do
|
679
|
+
expect(presenter.apply_filters_to_scope(scope, params, options).to_sql).to match(/id.\s*=\s*2/)
|
680
|
+
end
|
681
|
+
end
|
682
|
+
|
683
|
+
describe '#apply_ordering_to_scope' do
|
684
|
+
let(:presenter_class) { WorkspacePresenter }
|
685
|
+
let(:presenter) { presenter_class.new }
|
686
|
+
let(:scope) { Workspace.where(nil) }
|
687
|
+
|
688
|
+
it 'uses #calculate_sort_name_and_direction to extract a sort name and direction from user params' do
|
689
|
+
presenter_class.sort_order :title, 'workspaces.title'
|
690
|
+
mock(presenter).calculate_sort_name_and_direction('order' => 'title:desc') { ['title', 'desc'] }
|
691
|
+
presenter.apply_ordering_to_scope(scope, 'order' => 'title:desc')
|
692
|
+
end
|
693
|
+
|
694
|
+
context 'when the sort is a proc' do
|
695
|
+
it 'runs procs in the context of any helpers' do
|
696
|
+
presenter_class.helper do
|
697
|
+
def some_method
|
219
698
|
end
|
220
699
|
end
|
221
700
|
|
222
|
-
|
223
|
-
|
701
|
+
direction = nil
|
702
|
+
presenter_class.sort_order(:title) do |scope, d|
|
703
|
+
some_method
|
704
|
+
direction = d
|
705
|
+
scope
|
706
|
+
end
|
707
|
+
|
708
|
+
presenter.apply_ordering_to_scope(scope, 'order' => 'title:asc')
|
709
|
+
expect(direction).to eq 'asc'
|
224
710
|
end
|
225
711
|
|
226
|
-
it
|
227
|
-
|
228
|
-
|
712
|
+
it 'can chain multiple sorts together' do
|
713
|
+
presenter_class.sort_order(:title) do |scope|
|
714
|
+
scope.order('workspaces.title desc').order('workspaces.id desc')
|
715
|
+
end
|
716
|
+
|
717
|
+
sql = presenter.apply_ordering_to_scope(scope, 'order' => 'title').to_sql
|
718
|
+
expect(sql).to match(/order by workspaces.title desc, workspaces.id desc, "workspaces"."id" ASC/i)
|
719
|
+
# this should be ok, since the first id sort will never have a tie
|
229
720
|
end
|
230
721
|
|
231
|
-
it
|
232
|
-
|
233
|
-
|
722
|
+
it 'chains the primary key onto the end' do
|
723
|
+
presenter_class.sort_order(:title) do |scope|
|
724
|
+
scope.order('workspaces.title desc')
|
725
|
+
end
|
726
|
+
|
727
|
+
sql = presenter.apply_ordering_to_scope(scope, 'order' => 'title').to_sql
|
728
|
+
expect(sql).to match(/order by workspaces.title desc, "workspaces"."id" ASC/i)
|
234
729
|
end
|
730
|
+
end
|
235
731
|
|
236
|
-
|
237
|
-
|
732
|
+
context 'when the sort is not a proc' do
|
733
|
+
before do
|
734
|
+
presenter_class.sort_order :title, 'workspaces.title'
|
735
|
+
presenter_class.sort_order :id, 'workspaces.id'
|
238
736
|
end
|
239
737
|
|
240
|
-
|
241
|
-
|
738
|
+
context 'and is a string' do
|
739
|
+
let(:order) { { 'order' => 'title:asc' } }
|
740
|
+
|
741
|
+
it 'applies the named ordering in the given direction and adds the primary key as a fallback sort' do
|
742
|
+
sql = presenter.apply_ordering_to_scope(scope, order).to_sql
|
743
|
+
expect(sql).to match(/ORDER BY workspaces.title asc, "workspaces"."id" ASC/i)
|
744
|
+
end
|
242
745
|
end
|
243
746
|
|
244
|
-
|
245
|
-
|
246
|
-
|
747
|
+
context 'and is a symbol' do
|
748
|
+
let(:order) { { 'order' => :title } }
|
749
|
+
|
750
|
+
it 'applies the named ordering in the given direction and adds the primary key as a fallback sort' do
|
751
|
+
sql = presenter.apply_ordering_to_scope(scope, order).to_sql
|
752
|
+
expect(sql).to match(/order by workspaces.title asc, "workspaces"."id" ASC/i)
|
753
|
+
end
|
247
754
|
end
|
755
|
+
end
|
248
756
|
|
249
|
-
|
250
|
-
|
757
|
+
context 'when the sort is not present' do
|
758
|
+
let(:order) { '' }
|
251
759
|
|
252
|
-
|
253
|
-
|
254
|
-
expect(
|
255
|
-
expect(json[:something_id]).to eq(@workspace.user_id.to_s)
|
760
|
+
it 'orders by the primary key' do
|
761
|
+
sql = presenter.apply_ordering_to_scope(scope, order).to_sql
|
762
|
+
expect(sql).to match(/order by "workspaces"."id" ASC/i)
|
256
763
|
end
|
764
|
+
end
|
257
765
|
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
766
|
+
context 'when the table has no primary key' do
|
767
|
+
let(:scope) { Cthulhu.where(nil) }
|
768
|
+
let(:order) { { 'order' => 'updated_at:asc' } }
|
769
|
+
|
770
|
+
before do
|
771
|
+
class Cthulhu < Workspace
|
772
|
+
self.primary_key = nil
|
773
|
+
end
|
774
|
+
|
775
|
+
presenter_class.sort_order :updated_at, 'workspaces.updated_at'
|
265
776
|
end
|
266
777
|
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
778
|
+
it 'does not add a fallback deterministic sort, and you deserve whatever fate befalls you' do
|
779
|
+
sql = presenter.apply_ordering_to_scope(scope, order).to_sql.squish
|
780
|
+
expect(sql).to eq("SELECT \"workspaces\".* FROM \"workspaces\" WHERE \"workspaces\".\"type\" IN ('Cthulhu') ORDER BY workspaces.updated_at asc")
|
781
|
+
end
|
782
|
+
end
|
783
|
+
end
|
784
|
+
|
785
|
+
describe "#calculate_sort_name_and_direction" do
|
786
|
+
let(:presenter_class) { WorkspacePresenter }
|
787
|
+
let(:presenter) { presenter_class.new }
|
788
|
+
|
789
|
+
it 'uses default_sort_order by default when present' do
|
790
|
+
presenter_class.default_sort_order 'foo:asc'
|
791
|
+
expect(presenter.calculate_sort_name_and_direction).to eq ['foo', 'asc']
|
792
|
+
end
|
793
|
+
|
794
|
+
it 'uses updated_at:desc when no default has been set' do
|
795
|
+
expect(presenter.calculate_sort_name_and_direction).to eq ['updated_at', 'desc']
|
796
|
+
end
|
797
|
+
|
798
|
+
it 'ignores unknown sorts' do
|
799
|
+
presenter_class.sort_order :foo, 'workspaces.foo'
|
800
|
+
expect(presenter.calculate_sort_name_and_direction('order' => 'hello:desc')).to eq ['updated_at', 'desc']
|
801
|
+
expect(presenter.calculate_sort_name_and_direction('order' => 'foo:desc')).to eq ['foo', 'desc']
|
802
|
+
end
|
803
|
+
|
804
|
+
it 'sanitizes the direction' do
|
805
|
+
presenter_class.sort_order :foo, 'workspaces.foo'
|
806
|
+
expect(presenter.calculate_sort_name_and_direction('order' => 'foo:drop table')).to eq ['foo', 'asc']
|
807
|
+
expect(presenter.calculate_sort_name_and_direction('order' => 'foo:')).to eq ['foo', 'asc']
|
808
|
+
expect(presenter.calculate_sort_name_and_direction('order' => 'foo:hi')).to eq ['foo', 'asc']
|
809
|
+
expect(presenter.calculate_sort_name_and_direction('order' => 'foo:DESCE')).to eq ['foo', 'asc']
|
810
|
+
expect(presenter.calculate_sort_name_and_direction('order' => 'foo:asc')).to eq ['foo', 'asc']
|
811
|
+
expect(presenter.calculate_sort_name_and_direction('order' => 'foo:;;;droptable::;;')).to eq ['foo', 'asc']
|
812
|
+
end
|
813
|
+
end
|
814
|
+
|
815
|
+
describe '#allowed_associations' do
|
816
|
+
let(:presenter_class) do
|
817
|
+
Class.new(Brainstem::Presenter) do
|
818
|
+
associations do
|
819
|
+
association :user, User
|
820
|
+
association :workspace, Workspace
|
821
|
+
association :task, Task, restrict_to_only: true
|
273
822
|
end
|
274
823
|
end
|
275
824
|
end
|
825
|
+
|
826
|
+
let(:presenter_instance) { presenter_class.new }
|
827
|
+
|
828
|
+
it 'returns all associations that are not restrict_to_only' do
|
829
|
+
expect(presenter_instance.allowed_associations(is_only_query = false).keys).to match_array %w[user workspace]
|
830
|
+
end
|
831
|
+
|
832
|
+
it 'returns associations that are restrict_to_only if is_only_query is true' do
|
833
|
+
expect(presenter_instance.allowed_associations(is_only_query = true).keys).to match_array %w[user workspace task]
|
834
|
+
end
|
276
835
|
end
|
277
836
|
end
|