praxis 2.0.pre.5 → 2.0.pre.6
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 -5
- data/.rspec +0 -1
- data/.ruby-version +1 -0
- data/CHANGELOG.md +22 -0
- data/Gemfile +1 -1
- data/Guardfile +2 -1
- data/Rakefile +1 -7
- data/TODO.md +28 -0
- data/lib/api_browser/package-lock.json +7110 -0
- data/lib/praxis.rb +6 -4
- data/lib/praxis/action_definition.rb +9 -16
- data/lib/praxis/application.rb +1 -2
- data/lib/praxis/bootloader_stages/routing.rb +2 -4
- data/lib/praxis/extensions/attribute_filtering.rb +2 -0
- data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +148 -157
- data/lib/praxis/extensions/attribute_filtering/active_record_patches.rb +15 -0
- data/lib/praxis/extensions/attribute_filtering/active_record_patches/5x.rb +90 -0
- data/lib/praxis/extensions/attribute_filtering/active_record_patches/6_0.rb +68 -0
- data/lib/praxis/extensions/attribute_filtering/active_record_patches/6_1_plus.rb +58 -0
- data/lib/praxis/extensions/attribute_filtering/filter_tree_node.rb +35 -0
- data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +9 -12
- data/lib/praxis/extensions/attribute_filtering/sequel_filter_query_builder.rb +3 -2
- data/lib/praxis/extensions/field_selection/active_record_query_selector.rb +7 -9
- data/lib/praxis/extensions/field_selection/sequel_query_selector.rb +6 -9
- data/lib/praxis/extensions/pagination.rb +130 -0
- data/lib/praxis/extensions/pagination/active_record_pagination_handler.rb +42 -0
- data/lib/praxis/extensions/pagination/header_generator.rb +70 -0
- data/lib/praxis/extensions/pagination/ordering_params.rb +234 -0
- data/lib/praxis/extensions/pagination/pagination_handler.rb +68 -0
- data/lib/praxis/extensions/pagination/pagination_params.rb +374 -0
- data/lib/praxis/extensions/pagination/sequel_pagination_handler.rb +45 -0
- data/lib/praxis/handlers/json.rb +2 -0
- data/lib/praxis/handlers/www_form.rb +5 -0
- data/lib/praxis/handlers/{xml.rb → xml-sample.rb} +6 -0
- data/lib/praxis/mapper/active_model_compat.rb +23 -5
- data/lib/praxis/mapper/resource.rb +16 -9
- data/lib/praxis/mapper/sequel_compat.rb +1 -0
- data/lib/praxis/media_type.rb +1 -56
- data/lib/praxis/plugins/mapper_plugin.rb +1 -1
- data/lib/praxis/plugins/pagination_plugin.rb +71 -0
- data/lib/praxis/resource_definition.rb +4 -12
- data/lib/praxis/route.rb +2 -4
- data/lib/praxis/routing_config.rb +4 -8
- data/lib/praxis/tasks/routes.rb +9 -14
- data/lib/praxis/validation_handler.rb +1 -2
- data/lib/praxis/version.rb +1 -1
- data/praxis.gemspec +2 -3
- data/spec/functional_spec.rb +9 -6
- data/spec/praxis/action_definition_spec.rb +4 -16
- data/spec/praxis/api_general_info_spec.rb +6 -6
- data/spec/praxis/extensions/attribute_filtering/active_record_filter_query_builder_spec.rb +304 -0
- data/spec/praxis/extensions/attribute_filtering/filter_tree_node_spec.rb +39 -0
- data/spec/praxis/extensions/attribute_filtering/filtering_params_spec.rb +34 -0
- data/spec/praxis/extensions/field_expansion_spec.rb +6 -24
- data/spec/praxis/extensions/field_selection/active_record_query_selector_spec.rb +15 -11
- data/spec/praxis/extensions/field_selection/sequel_query_selector_spec.rb +4 -3
- data/spec/praxis/extensions/pagination/active_record_pagination_handler_spec.rb +130 -0
- data/spec/praxis/extensions/{field_selection/support → support}/spec_resources_active_model.rb +45 -2
- data/spec/praxis/extensions/{field_selection/support → support}/spec_resources_sequel.rb +0 -0
- data/spec/praxis/media_type_spec.rb +5 -129
- data/spec/praxis/request_spec.rb +3 -22
- data/spec/praxis/resource_definition_spec.rb +1 -1
- data/spec/praxis/response_definition_spec.rb +1 -5
- data/spec/praxis/route_spec.rb +2 -9
- data/spec/praxis/routing_config_spec.rb +4 -13
- data/spec/praxis/types/multipart_array_spec.rb +4 -21
- data/spec/spec_app/config/environment.rb +0 -2
- data/spec/spec_app/design/api.rb +1 -1
- data/spec/spec_app/design/media_types/instance.rb +0 -8
- data/spec/spec_app/design/media_types/volume.rb +0 -12
- data/spec/spec_app/design/resources/instances.rb +1 -2
- data/spec/spec_helper.rb +6 -0
- data/spec/support/spec_media_types.rb +0 -73
- metadata +35 -45
- data/spec/praxis/handlers/xml_spec.rb +0 -177
- data/spec/praxis/links_spec.rb +0 -68
data/lib/praxis/tasks/routes.rb
CHANGED
@@ -7,7 +7,7 @@ namespace :praxis do
|
|
7
7
|
table = Terminal::Table.new title: "Routes",
|
8
8
|
headings: [
|
9
9
|
"Version", "Path", "Verb",
|
10
|
-
"Resource", "Action", "Implementation", "
|
10
|
+
"Resource", "Action", "Implementation", "Options"
|
11
11
|
]
|
12
12
|
|
13
13
|
rows = []
|
@@ -31,20 +31,16 @@ namespace :praxis do
|
|
31
31
|
warn "Warning: No routes defined for #{resource_definition.name}##{name}."
|
32
32
|
rows << row
|
33
33
|
else
|
34
|
-
action.
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
options: route.options
|
42
|
-
})
|
34
|
+
route = action.route
|
35
|
+
rows << row.merge({
|
36
|
+
version: route.version,
|
37
|
+
verb: route.verb,
|
38
|
+
path: route.path,
|
39
|
+
options: route.options
|
40
|
+
})
|
43
41
|
end
|
44
42
|
end
|
45
43
|
end
|
46
|
-
end
|
47
|
-
|
48
44
|
case args[:format] || "table"
|
49
45
|
when "json"
|
50
46
|
puts JSON.pretty_generate(rows)
|
@@ -52,7 +48,7 @@ namespace :praxis do
|
|
52
48
|
rows.each do |row|
|
53
49
|
formatted_options = row[:options].map{|(k,v)| "#{k}:#{v.to_s}"}.join("\n")
|
54
50
|
row_data = row.values_at(:version, :path, :verb, :resource,
|
55
|
-
:action, :implementation
|
51
|
+
:action, :implementation)
|
56
52
|
row_data << formatted_options
|
57
53
|
table.add_row(row_data)
|
58
54
|
end
|
@@ -60,6 +56,5 @@ namespace :praxis do
|
|
60
56
|
else
|
61
57
|
raise "unknown output format: #{args[:format]}"
|
62
58
|
end
|
63
|
-
|
64
59
|
end
|
65
60
|
end
|
@@ -3,8 +3,7 @@ module Praxis
|
|
3
3
|
|
4
4
|
# Should return the Response to send back
|
5
5
|
def handle!(summary:, request:, stage:, errors: nil, exception: nil, **opts)
|
6
|
-
|
7
|
-
Responses::ValidationError.new(summary: summary, errors: errors, exception: exception, documentation: documentation, **opts)
|
6
|
+
Responses::ValidationError.new(summary: summary, errors: errors, exception: exception, **opts)
|
8
7
|
end
|
9
8
|
|
10
9
|
end
|
data/lib/praxis/version.rb
CHANGED
data/praxis.gemspec
CHANGED
@@ -30,8 +30,8 @@ Gem::Specification.new do |spec|
|
|
30
30
|
spec.add_dependency 'terminal-table', '~> 1.4'
|
31
31
|
|
32
32
|
spec.add_development_dependency 'bundler'
|
33
|
-
spec.add_development_dependency 'rake', '
|
34
|
-
|
33
|
+
spec.add_development_dependency 'rake', '>= 12.3.3'
|
34
|
+
|
35
35
|
if RUBY_PLATFORM !~ /java/
|
36
36
|
spec.add_development_dependency 'pry'
|
37
37
|
spec.add_development_dependency 'pry-byebug'
|
@@ -49,7 +49,6 @@ Gem::Specification.new do |spec|
|
|
49
49
|
spec.add_development_dependency 'rack-test', '~> 0'
|
50
50
|
spec.add_development_dependency 'simplecov', '~> 0'
|
51
51
|
spec.add_development_dependency 'fuubar', '~> 2'
|
52
|
-
spec.add_development_dependency 'yard', ">= 0.9.20"
|
53
52
|
spec.add_development_dependency 'coveralls'
|
54
53
|
# Just for the query selector extensions etc...
|
55
54
|
spec.add_development_dependency 'sequel', '~> 5'
|
data/spec/functional_spec.rb
CHANGED
@@ -331,12 +331,15 @@ describe 'Functional specs' do
|
|
331
331
|
end
|
332
332
|
|
333
333
|
context 'wildcard verb routing' do
|
334
|
+
let(:content_type){ 'application/json' }
|
334
335
|
it 'can terminate instances with POST' do
|
335
|
-
post '/api/clouds/23/instances/1/terminate?api_version=1.0', '', 'global_session' => session
|
336
|
+
post '/api/clouds/23/instances/1/terminate?api_version=1.0', nil, 'CONTENT_TYPE' => content_type, 'global_session' => session
|
337
|
+
puts last_response.body
|
338
|
+
#binding.pry
|
336
339
|
expect(last_response.status).to eq(200)
|
337
340
|
end
|
338
341
|
it 'can terminate instances with DELETE' do
|
339
|
-
post '/api/clouds/23/instances/1/terminate?api_version=1.0', '', 'global_session' => session
|
342
|
+
post '/api/clouds/23/instances/1/terminate?api_version=1.0', nil, 'CONTENT_TYPE' => content_type, 'global_session' => session
|
340
343
|
expect(last_response.status).to eq(200)
|
341
344
|
end
|
342
345
|
|
@@ -355,7 +358,7 @@ describe 'Functional specs' do
|
|
355
358
|
|
356
359
|
context 'auth_plugin' do
|
357
360
|
it 'can terminate' do
|
358
|
-
post '/api/clouds/23/instances/1/terminate?api_version=1.0',
|
361
|
+
post '/api/clouds/23/instances/1/terminate?api_version=1.0', nil, 'global_session' => session
|
359
362
|
expect(last_response.status).to eq(200)
|
360
363
|
end
|
361
364
|
|
@@ -366,8 +369,8 @@ describe 'Functional specs' do
|
|
366
369
|
end
|
367
370
|
|
368
371
|
context 'with mismatch between Content-Type and payload' do
|
369
|
-
let(:body) { '
|
370
|
-
let(:content_type) { 'application/
|
372
|
+
let(:body) { 'some-text' }
|
373
|
+
let(:content_type) { 'application/json' }
|
371
374
|
|
372
375
|
before do
|
373
376
|
post '/api/clouds/1/instances/2/terminate?api_version=1.0', body, 'CONTENT_TYPE' => content_type, 'global_session' => session
|
@@ -376,7 +379,7 @@ describe 'Functional specs' do
|
|
376
379
|
it 'returns a useful error message' do
|
377
380
|
body = JSON.parse(last_response.body)
|
378
381
|
expect(body['name']).to eq('ValidationError')
|
379
|
-
expect(body['summary']).to match("Error loading payload. Used Content-Type: 'application/
|
382
|
+
expect(body['summary']).to match("Error loading payload. Used Content-Type: 'application/json'")
|
380
383
|
expect(body['errors']).to_not be_empty
|
381
384
|
end
|
382
385
|
end
|
@@ -115,7 +115,7 @@ describe Praxis::ActionDefinition do
|
|
115
115
|
end
|
116
116
|
|
117
117
|
its('params.attributes.keys') { should eq [:inherited, :app_name, :name, :one]}
|
118
|
-
its('
|
118
|
+
its('route.path.to_s') { should eq '/api/foobars/hello_world/test_trait/:app_name/:one' }
|
119
119
|
its(:traits) { should eq [:test] }
|
120
120
|
|
121
121
|
it 'is reflected in the describe output' do
|
@@ -231,7 +231,7 @@ describe Praxis::ActionDefinition do
|
|
231
231
|
let(:parent_param) { ApiResources::Volumes.actions[:show].params.attributes[:id] }
|
232
232
|
|
233
233
|
it 'has the right path' do
|
234
|
-
expect(action.
|
234
|
+
expect(action.route.path.to_s).to eq '/api/clouds/:cloud_id/volumes/:volume_id/snapshots/:id'
|
235
235
|
end
|
236
236
|
|
237
237
|
its('params.attributes'){ should have_key(:cloud_id) }
|
@@ -281,22 +281,10 @@ describe Praxis::ActionDefinition do
|
|
281
281
|
subject(:action) { resource_definition.actions[:show] }
|
282
282
|
|
283
283
|
it 'works' do
|
284
|
-
expansion = action.
|
284
|
+
expansion = action.route.path.expand(cloud_id:232, id: 2)
|
285
285
|
expect(expansion).to eq "/api/clouds/232/instances/2"
|
286
286
|
end
|
287
287
|
|
288
|
-
context '#primary_route' do
|
289
|
-
it 'is the first-defined route' do
|
290
|
-
expect(action.primary_route).to be(action.routes.first)
|
291
|
-
end
|
292
|
-
end
|
293
|
-
|
294
|
-
context '#named_routes' do
|
295
|
-
subject(:named_routes) { action.named_routes }
|
296
|
-
|
297
|
-
its([:alternate]) { should be(action.routes[1]) }
|
298
|
-
end
|
299
|
-
|
300
288
|
end
|
301
289
|
|
302
290
|
context 'with nodoc!' do
|
@@ -339,7 +327,7 @@ describe Praxis::ActionDefinition do
|
|
339
327
|
allow(Praxis::ApiDefinition).to receive(:instance).and_return(non_singleton_api)
|
340
328
|
end
|
341
329
|
|
342
|
-
its('
|
330
|
+
its('route.path.to_s') { should eq '/apps/:app_name/foobars/hello_world/:one' }
|
343
331
|
its('params.attributes.keys') { should match_array [:inherited, :app_name, :one]}
|
344
332
|
|
345
333
|
context 'where the action overrides a base_param' do
|
@@ -17,8 +17,8 @@ describe Praxis::ApiGeneralInfo do
|
|
17
17
|
endpoint 'api.example.com'
|
18
18
|
base_path "/base"
|
19
19
|
|
20
|
-
consumes '
|
21
|
-
produces 'json'
|
20
|
+
consumes 'json'
|
21
|
+
produces 'json'
|
22
22
|
|
23
23
|
base_params do
|
24
24
|
attribute :name, String
|
@@ -40,8 +40,8 @@ describe Praxis::ApiGeneralInfo do
|
|
40
40
|
end
|
41
41
|
|
42
42
|
its(:name) { should eq 'Name' }
|
43
|
-
its(:consumes) { should eq ['
|
44
|
-
its(:produces) { should eq ['json'
|
43
|
+
its(:consumes) { should eq ['json']}
|
44
|
+
its(:produces) { should eq ['json']}
|
45
45
|
end
|
46
46
|
|
47
47
|
context '.describe' do
|
@@ -59,8 +59,8 @@ describe Praxis::ApiGeneralInfo do
|
|
59
59
|
its([:base_params, :name, :type, :name]) { should eq 'String' }
|
60
60
|
its([:version_with]) { should eq([:header, :params]) }
|
61
61
|
its([:endpoint]) { should eq 'api.example.com' }
|
62
|
-
its([:consumes]) { should eq ['
|
63
|
-
its([:produces]) { should eq ['json'
|
62
|
+
its([:consumes]) { should eq ['json'] }
|
63
|
+
its([:produces]) { should eq ['json'] }
|
64
64
|
end
|
65
65
|
|
66
66
|
context 'base_path with versioning' do
|
@@ -0,0 +1,304 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
require_relative '../support/spec_resources_active_model.rb'
|
4
|
+
require 'praxis/extensions/attribute_filtering'
|
5
|
+
require 'praxis/extensions/attribute_filtering/active_record_filter_query_builder'
|
6
|
+
|
7
|
+
describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder do
|
8
|
+
let(:root_resource) { ActiveBookResource }
|
9
|
+
let(:filters_map) { root_resource.instance_variable_get(:@_filters_map)}
|
10
|
+
let(:base_model) { root_resource.model }
|
11
|
+
let(:base_query) { base_model }
|
12
|
+
let(:instance) { described_class.new(query: base_query, model: base_model, filters_map: filters_map) }
|
13
|
+
|
14
|
+
shared_examples 'subject_equivalent_to' do |expected_result|
|
15
|
+
it do
|
16
|
+
loaded_ids = subject.all.map(&:id).sort
|
17
|
+
expected_ids = expected_result.all.map(&:id).sort
|
18
|
+
expect(loaded_ids).to_not be_empty
|
19
|
+
expect(loaded_ids).to eq(expected_ids)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Poorman's way to compare SQL queries...
|
24
|
+
shared_examples 'subject_matches_sql' do |expected_sql|
|
25
|
+
it do
|
26
|
+
# Remove parenthesis as our queries have WHERE clauses using them...
|
27
|
+
gen_sql = subject.all.to_sql.gsub(/[()]/,'')
|
28
|
+
# Strip blank at the beggining (and end) of every line
|
29
|
+
# ...and recompose it by adding an extra space at the beginning of each one instead
|
30
|
+
exp = expected_sql.split(/\n/).map do |line|
|
31
|
+
" " + line.strip.gsub(/[()]/,'')
|
32
|
+
end.join.strip
|
33
|
+
expect(gen_sql).to eq(exp)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context 'initialize' do
|
38
|
+
it 'sets the right things to the instance' do
|
39
|
+
instance
|
40
|
+
expect(instance.query).to eq(base_query)
|
41
|
+
expect(instance.model).to eq(base_model)
|
42
|
+
expect(instance.attr_to_column).to eq(filters_map)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
context 'generate' do
|
46
|
+
subject { instance.generate(filters) }
|
47
|
+
let(:filters) { Praxis::Types::FilteringParams.load(filters_string)}
|
48
|
+
|
49
|
+
context 'with no filters' do
|
50
|
+
let(:filters_string) { '' }
|
51
|
+
it 'does not modify the query' do
|
52
|
+
expect(subject).to be(base_query)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
context 'by a simple field' do
|
56
|
+
context 'that maps to the same name' do
|
57
|
+
let(:filters_string) { 'category_uuid=deadbeef1' }
|
58
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.where(category_uuid: 'deadbeef1')
|
59
|
+
end
|
60
|
+
context 'that maps to a different name' do
|
61
|
+
let(:filters_string) { 'name=Book1'}
|
62
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.where(simple_name: 'Book1')
|
63
|
+
end
|
64
|
+
context 'that is mapped as a nested struct' do
|
65
|
+
let(:filters_string) { 'fake_nested.name=Book1'}
|
66
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.where(simple_name: 'Book1')
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
context 'by a field or a related model' do
|
71
|
+
context 'for a belongs_to association' do
|
72
|
+
let(:filters_string) { 'author.name=author2'}
|
73
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.name' => 'author2')
|
74
|
+
end
|
75
|
+
context 'for a has_many association' do
|
76
|
+
let(:filters_string) { 'taggings.label=primary' }
|
77
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:taggings).where('active_taggings.label' => 'primary')
|
78
|
+
end
|
79
|
+
context 'for a has_many through association' do
|
80
|
+
let(:filters_string) { 'tags.name=blue' }
|
81
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:tags).where('active_tags.name' => 'blue')
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
context 'by using all supported operators' do
|
86
|
+
PREF = Praxis::Extensions::AttributeFiltering::ALIAS_TABLE_PREFIX
|
87
|
+
COMMON_SQL_PREFIX = <<~SQL
|
88
|
+
SELECT "active_books".* FROM "active_books"
|
89
|
+
INNER JOIN
|
90
|
+
"active_authors" "#{PREF}/author" ON "#{PREF}/author"."id" = "active_books"."author_id"
|
91
|
+
SQL
|
92
|
+
context '=' do
|
93
|
+
let(:filters_string) { 'author.id=11'}
|
94
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id = 11')
|
95
|
+
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
96
|
+
WHERE "#{PREF}/author"."id" = 11
|
97
|
+
SQL
|
98
|
+
end
|
99
|
+
context '= (with array)' do
|
100
|
+
let(:filters_string) { 'author.id=11,22'}
|
101
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id IN (11,22)')
|
102
|
+
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
103
|
+
WHERE "#{PREF}/author"."id" IN (11,22)
|
104
|
+
SQL
|
105
|
+
end
|
106
|
+
context '!=' do
|
107
|
+
let(:filters_string) { 'author.id!=11'}
|
108
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id <> 11')
|
109
|
+
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
110
|
+
WHERE "#{PREF}/author"."id" <> 11
|
111
|
+
SQL
|
112
|
+
end
|
113
|
+
context '!= (with array)' do
|
114
|
+
let(:filters_string) { 'author.id!=11,888'}
|
115
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id NOT IN (11,888)')
|
116
|
+
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
117
|
+
WHERE "#{PREF}/author"."id" NOT IN (11,888)
|
118
|
+
SQL
|
119
|
+
end
|
120
|
+
context '>' do
|
121
|
+
let(:filters_string) { 'author.id>1'}
|
122
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id > 1')
|
123
|
+
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
124
|
+
WHERE "#{PREF}/author"."id" > 1
|
125
|
+
SQL
|
126
|
+
end
|
127
|
+
context '<' do
|
128
|
+
let(:filters_string) { 'author.id<22'}
|
129
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id < 22')
|
130
|
+
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
131
|
+
WHERE "#{PREF}/author"."id" < 22
|
132
|
+
SQL
|
133
|
+
end
|
134
|
+
context '>=' do
|
135
|
+
let(:filters_string) { 'author.id>=22'}
|
136
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id >= 22')
|
137
|
+
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
138
|
+
WHERE "#{PREF}/author"."id" >= 22
|
139
|
+
SQL
|
140
|
+
end
|
141
|
+
context '<=' do
|
142
|
+
let(:filters_string) { 'author.id<=22'}
|
143
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id <= 22')
|
144
|
+
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
145
|
+
WHERE "#{PREF}/author"."id" <= 22
|
146
|
+
SQL
|
147
|
+
end
|
148
|
+
context '!' do
|
149
|
+
let(:filters_string) { 'author.id!'}
|
150
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id IS NOT NULL')
|
151
|
+
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
152
|
+
WHERE "#{PREF}/author"."id" IS NOT NULL
|
153
|
+
SQL
|
154
|
+
end
|
155
|
+
context '!!' do
|
156
|
+
let(:filters_string) { 'author.name!!'}
|
157
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.name IS NULL')
|
158
|
+
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
159
|
+
WHERE "#{PREF}/author"."name" IS NULL
|
160
|
+
SQL
|
161
|
+
end
|
162
|
+
context 'including LIKE fuzzy queries' do
|
163
|
+
context 'LIKE' do
|
164
|
+
let(:filters_string) { 'author.name=author*'}
|
165
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.name LIKE "author%"')
|
166
|
+
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
167
|
+
WHERE "#{PREF}/author"."name" LIKE 'author%'
|
168
|
+
SQL
|
169
|
+
end
|
170
|
+
context 'NOT LIKE' do
|
171
|
+
let(:filters_string) { 'author.name!=foobar*'}
|
172
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.name NOT LIKE "foobar%"')
|
173
|
+
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
174
|
+
WHERE "#{PREF}/author"."name" NOT LIKE 'foobar%'
|
175
|
+
SQL
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
context 'with a field mapping using a proc' do
|
181
|
+
let(:filters_string) { 'name_is_not=Book1' }
|
182
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.where.not(simple_name: 'Book1')
|
183
|
+
end
|
184
|
+
|
185
|
+
context 'with a deeply nested chains' do
|
186
|
+
context 'of depth 2' do
|
187
|
+
let(:filters_string) { 'category.books.name=Book2' }
|
188
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(category: :books).where('books_active_categories.simple_name': 'Book2')
|
189
|
+
end
|
190
|
+
context 'multiple conditions on a nested relationship' do
|
191
|
+
let(:filters_string) { 'category.books.taggings.tag_id=1&category.books.taggings.label=primary' }
|
192
|
+
it_behaves_like 'subject_equivalent_to',
|
193
|
+
ActiveBook.joins(category: { books: :taggings }).where('active_taggings.tag_id': 1).where('active_taggings.label': 'primary')
|
194
|
+
it_behaves_like 'subject_matches_sql', <<~SQL
|
195
|
+
SELECT "active_books".* FROM "active_books"
|
196
|
+
INNER JOIN "active_categories" ON "active_categories"."uuid" = "active_books"."category_uuid"
|
197
|
+
INNER JOIN "active_books" "books_active_categories" ON "books_active_categories"."category_uuid" = "active_categories"."uuid"
|
198
|
+
INNER JOIN "active_taggings" "#{PREF}/category/books/taggings" ON "/category/books/taggings"."book_id" = "books_active_categories"."id"
|
199
|
+
WHERE ("#{PREF}/category/books/taggings"."tag_id" = 1)
|
200
|
+
AND ("#{PREF}/category/books/taggings"."label" = 'primary')
|
201
|
+
SQL
|
202
|
+
end
|
203
|
+
context 'that contain multiple joins to the same table' do
|
204
|
+
let(:filters_string) { 'taggings.tag.taggings.tag_id=1' }
|
205
|
+
it_behaves_like 'subject_equivalent_to',
|
206
|
+
ActiveBook.joins(taggings: {tag: :taggings}).where('taggings_active_tags.tag_id=1')
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
context 'by multiple fields' do
|
211
|
+
context 'adds the where clauses for the top model if fields belong to it' do
|
212
|
+
let(:filters_string) { 'category_uuid=deadbeef1&name=Book1' }
|
213
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.where(category_uuid: 'deadbeef1', simple_name: 'Book1')
|
214
|
+
end
|
215
|
+
context 'adds multiple where clauses for same nested relationship join (instead of multiple joins with 1 clause each)' do
|
216
|
+
let(:filters_string) { 'taggings.label=primary&taggings.tag_id=2' }
|
217
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:taggings).where('active_taggings.label' => 'primary', 'active_taggings.tag_id' => 2)
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
context 'uses fully qualified names for conditions (disambiguate fields)' do
|
222
|
+
context 'when we have a join table condition that has the same field' do
|
223
|
+
COMMON_SQL_PREFIX = <<~SQL
|
224
|
+
SELECT "active_books".* FROM "active_books"
|
225
|
+
INNER JOIN "active_categories" ON "active_categories"."uuid" = "active_books"."category_uuid"
|
226
|
+
INNER JOIN "active_books" "#{PREF}/category/books" ON "#{PREF}/category/books"."category_uuid" = "active_categories"."uuid"
|
227
|
+
SQL
|
228
|
+
let(:filters_string) { 'name=Book1&category.books.name=Book3' }
|
229
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(category: :books)
|
230
|
+
.where('simple_name': 'Book1')
|
231
|
+
.where('books_active_categories.simple_name': 'Book3')
|
232
|
+
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
233
|
+
WHERE ("#{PREF}/category/books"."simple_name" = 'Book3')
|
234
|
+
AND ("active_books"."simple_name" = 'Book1')
|
235
|
+
SQL
|
236
|
+
end
|
237
|
+
|
238
|
+
context 'it qualifis them even if there are no joined tables/conditions at all' do
|
239
|
+
let(:filters_string) { 'id=11'}
|
240
|
+
it_behaves_like 'subject_matches_sql', <<~SQL
|
241
|
+
SELECT "active_books".* FROM "active_books"
|
242
|
+
WHERE "active_books"."id" = 11
|
243
|
+
SQL
|
244
|
+
end
|
245
|
+
|
246
|
+
end
|
247
|
+
|
248
|
+
context 'ActiveRecord continues to work as expected (with our patches)' do
|
249
|
+
context 'using a deep join with repeated tables' do
|
250
|
+
subject{ ActiveBook.joins(taggings: {tag: :taggings}).where('taggings_active_tags.tag_id=1') }
|
251
|
+
it 'performs query' do
|
252
|
+
expect(subject.to_a).to_not be_empty
|
253
|
+
end
|
254
|
+
it_behaves_like 'subject_matches_sql', <<~SQL
|
255
|
+
SELECT "active_books".* FROM "active_books"
|
256
|
+
INNER JOIN "active_taggings" ON "active_taggings"."book_id" = "active_books"."id"
|
257
|
+
INNER JOIN "active_tags" ON "active_tags"."id" = "active_taggings"."tag_id"
|
258
|
+
INNER JOIN "active_taggings" "taggings_active_tags" ON "taggings_active_tags"."tag_id" = "active_tags"."id"
|
259
|
+
WHERE (taggings_active_tags.tag_id=1)
|
260
|
+
SQL
|
261
|
+
end
|
262
|
+
context 'a deep join with repeated tables with the root AND the join, along with :through joins as well' do
|
263
|
+
subject!{ ActiveBook.joins(tags: {books: {taggings: :book}}).where('books_active_taggings.simple_name="Book2"') }
|
264
|
+
it 'performs query' do
|
265
|
+
expect(subject.to_a).to_not be_empty
|
266
|
+
end
|
267
|
+
it_behaves_like 'subject_matches_sql', <<~SQL
|
268
|
+
SELECT "active_books".* FROM "active_books"
|
269
|
+
INNER JOIN "active_taggings" ON "active_taggings"."book_id" = "active_books"."id"
|
270
|
+
INNER JOIN "active_tags" ON "active_tags"."id" = "active_taggings"."tag_id"
|
271
|
+
INNER JOIN "active_taggings" "taggings_active_tags_join" ON "taggings_active_tags_join"."tag_id" = "active_tags"."id"
|
272
|
+
INNER JOIN "active_books" "books_active_tags" ON "books_active_tags"."id" = "taggings_active_tags_join"."book_id"
|
273
|
+
INNER JOIN "active_taggings" "taggings_active_books" ON "taggings_active_books"."book_id" = "books_active_tags"."id"
|
274
|
+
INNER JOIN "active_books" "books_active_taggings" ON "books_active_taggings"."id" = "taggings_active_books"."book_id"
|
275
|
+
WHERE (books_active_taggings.simple_name="Book2")
|
276
|
+
SQL
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
context 'respects scopes' do
|
281
|
+
context 'for a has_many through association' do
|
282
|
+
let(:filters_string) { 'primary_tags.name=blue' }
|
283
|
+
it_behaves_like 'subject_equivalent_to',
|
284
|
+
ActiveBook.joins(:primary_tags).where('active_tags.name="blue"')
|
285
|
+
|
286
|
+
it 'adds the association scope clause to the join' do
|
287
|
+
inner_join_pieces = subject.to_sql.split('INNER')
|
288
|
+
found = inner_join_pieces.any? do |line|
|
289
|
+
line =~ /\s+JOIN "active_taggings".+ON.+\."label" = 'primary'/
|
290
|
+
end
|
291
|
+
expect(found).to be_truthy
|
292
|
+
end
|
293
|
+
# This is slightly incorrect in AR 6.1+ (since the picked aliases for active_taggings tables vary)
|
294
|
+
# it_behaves_like 'subject_matches_sql', <<~SQL
|
295
|
+
# SELECT "active_books".* FROM "active_books"
|
296
|
+
# INNER JOIN "active_taggings" ON "active_taggings"."label" = 'primary'
|
297
|
+
# AND "active_taggings"."book_id" = "active_books"."id"
|
298
|
+
# INNER JOIN "active_tags" "/primary_tags" ON "/primary_tags"."id" = "active_taggings"."tag_id"
|
299
|
+
# WHERE ("/primary_tags"."name" = 'blue')
|
300
|
+
# SQL
|
301
|
+
end
|
302
|
+
end
|
303
|
+
end
|
304
|
+
end
|