praxis 2.0.pre.10 → 2.0.pre.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/.travis.yml +1 -3
- data/CHANGELOG.md +26 -0
- data/bin/praxis +65 -2
- data/lib/praxis/api_definition.rb +8 -4
- data/lib/praxis/bootloader_stages/environment.rb +1 -0
- data/lib/praxis/collection.rb +11 -0
- data/lib/praxis/docs/open_api/response_object.rb +21 -6
- data/lib/praxis/docs/open_api_generator.rb +1 -1
- data/lib/praxis/extensions/attribute_filtering.rb +14 -1
- data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +206 -66
- data/lib/praxis/extensions/attribute_filtering/filter_tree_node.rb +3 -2
- data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +45 -41
- data/lib/praxis/extensions/attribute_filtering/filters_parser.rb +193 -0
- data/lib/praxis/extensions/attribute_filtering/sequel_filter_query_builder.rb +20 -8
- data/lib/praxis/extensions/pagination.rb +5 -32
- data/lib/praxis/mapper/active_model_compat.rb +4 -0
- data/lib/praxis/mapper/resource.rb +18 -2
- data/lib/praxis/mapper/selector_generator.rb +1 -0
- data/lib/praxis/mapper/sequel_compat.rb +7 -0
- data/lib/praxis/media_type_identifier.rb +11 -1
- data/lib/praxis/plugins/mapper_plugin.rb +22 -13
- data/lib/praxis/plugins/pagination_plugin.rb +34 -4
- data/lib/praxis/response_definition.rb +46 -66
- data/lib/praxis/responses/http.rb +3 -1
- data/lib/praxis/tasks/api_docs.rb +4 -1
- data/lib/praxis/tasks/routes.rb +6 -6
- data/lib/praxis/version.rb +1 -1
- data/spec/praxis/action_definition_spec.rb +3 -1
- data/spec/praxis/extensions/attribute_filtering/active_record_filter_query_builder_spec.rb +267 -167
- data/spec/praxis/extensions/attribute_filtering/filter_tree_node_spec.rb +25 -6
- data/spec/praxis/extensions/attribute_filtering/filtering_params_spec.rb +100 -17
- data/spec/praxis/extensions/attribute_filtering/filters_parser_spec.rb +148 -0
- data/spec/praxis/extensions/field_selection/active_record_query_selector_spec.rb +1 -1
- data/spec/praxis/extensions/field_selection/sequel_query_selector_spec.rb +1 -1
- data/spec/praxis/extensions/support/spec_resources_active_model.rb +1 -1
- data/spec/praxis/mapper/selector_generator_spec.rb +1 -1
- data/spec/praxis/media_type_identifier_spec.rb +15 -1
- data/spec/praxis/response_definition_spec.rb +37 -129
- data/tasks/thor/example.rb +12 -6
- data/tasks/thor/model.rb +40 -0
- data/tasks/thor/scaffold.rb +117 -0
- data/tasks/thor/templates/generator/empty_app/config/environment.rb +1 -0
- data/tasks/thor/templates/generator/example_app/Rakefile +9 -2
- data/tasks/thor/templates/generator/example_app/app/v1/concerns/controller_base.rb +24 -0
- data/tasks/thor/templates/generator/example_app/app/v1/concerns/href.rb +33 -0
- data/tasks/thor/templates/generator/example_app/app/v1/controllers/users.rb +2 -2
- data/tasks/thor/templates/generator/example_app/app/v1/resources/base.rb +15 -0
- data/tasks/thor/templates/generator/example_app/app/v1/resources/user.rb +7 -28
- data/tasks/thor/templates/generator/example_app/config.ru +1 -2
- data/tasks/thor/templates/generator/example_app/config/environment.rb +3 -2
- data/tasks/thor/templates/generator/example_app/db/migrate/20201010101010_create_users_table.rb +3 -2
- data/tasks/thor/templates/generator/example_app/db/seeds.rb +6 -0
- data/tasks/thor/templates/generator/example_app/design/v1/endpoints/users.rb +4 -4
- data/tasks/thor/templates/generator/example_app/design/v1/media_types/user.rb +1 -6
- data/tasks/thor/templates/generator/example_app/spec/helpers/database_helper.rb +4 -2
- data/tasks/thor/templates/generator/example_app/spec/spec_helper.rb +2 -2
- data/tasks/thor/templates/generator/example_app/spec/v1/controllers/users_spec.rb +2 -2
- data/tasks/thor/templates/generator/scaffold/design/endpoints/collection.rb +98 -0
- data/tasks/thor/templates/generator/scaffold/design/media_types/item.rb +18 -0
- data/tasks/thor/templates/generator/scaffold/implementation/controllers/collection.rb +77 -0
- data/tasks/thor/templates/generator/scaffold/implementation/resources/base.rb +11 -0
- data/tasks/thor/templates/generator/scaffold/implementation/resources/item.rb +45 -0
- data/tasks/thor/templates/generator/scaffold/models/active_record.rb +6 -0
- data/tasks/thor/templates/generator/scaffold/models/sequel.rb +6 -0
- metadata +21 -6
data/lib/praxis/version.rb
CHANGED
@@ -40,7 +40,9 @@ describe Praxis::ActionDefinition do
|
|
40
40
|
|
41
41
|
media_type media_type
|
42
42
|
location location
|
43
|
-
headers
|
43
|
+
headers&.each do |(name, value)|
|
44
|
+
header(name, value)
|
45
|
+
end
|
44
46
|
end
|
45
47
|
end
|
46
48
|
Praxis::ActionDefinition.new(:foo, endpoint_definition) do
|
@@ -14,6 +14,7 @@ describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
|
|
14
14
|
shared_examples 'subject_equivalent_to' do |expected_result|
|
15
15
|
it do
|
16
16
|
loaded_ids = subject.all.map(&:id).sort
|
17
|
+
expected_result = expected_result.call if expected_result.is_a?(Proc)
|
17
18
|
expected_ids = expected_result.all.map(&:id).sort
|
18
19
|
expect(loaded_ids).to_not be_empty
|
19
20
|
expect(loaded_ids).to eq(expected_ids)
|
@@ -23,12 +24,11 @@ describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
|
|
23
24
|
# Poorman's way to compare SQL queries...
|
24
25
|
shared_examples 'subject_matches_sql' do |expected_sql|
|
25
26
|
it do
|
26
|
-
|
27
|
-
gen_sql = subject.all.to_sql.gsub(/[()]/,'')
|
27
|
+
gen_sql = subject.all.to_sql
|
28
28
|
# Strip blank at the beggining (and end) of every line
|
29
29
|
# ...and recompose it by adding an extra space at the beginning of each one instead
|
30
30
|
exp = expected_sql.split(/\n/).map do |line|
|
31
|
-
" " + line.strip
|
31
|
+
" " + line.strip
|
32
32
|
end.join.strip
|
33
33
|
expect(gen_sql).to eq(exp)
|
34
34
|
end
|
@@ -37,9 +37,9 @@ describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
|
|
37
37
|
context 'initialize' do
|
38
38
|
it 'sets the right things to the instance' do
|
39
39
|
instance
|
40
|
-
expect(instance.
|
40
|
+
expect(instance.instance_variable_get(:@initial_query)).to eq(base_query)
|
41
41
|
expect(instance.model).to eq(base_model)
|
42
|
-
expect(instance.
|
42
|
+
expect(instance.filters_map).to eq(filters_map)
|
43
43
|
end
|
44
44
|
end
|
45
45
|
context 'generate' do
|
@@ -52,195 +52,295 @@ describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
|
|
52
52
|
expect(subject).to be(base_query)
|
53
53
|
end
|
54
54
|
end
|
55
|
-
context '
|
56
|
-
context '
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
it_behaves_like 'subject_equivalent_to', ActiveBook.where(simple_name: 'Book1')
|
67
|
-
end
|
68
|
-
end
|
55
|
+
context 'with flat AND conditions' do
|
56
|
+
context 'by a simple field' do
|
57
|
+
context 'that maps to the same name' do
|
58
|
+
let(:filters_string) { 'category_uuid=deadbeef1' }
|
59
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.where(category_uuid: 'deadbeef1')
|
60
|
+
end
|
61
|
+
context 'same-name filter mapping works' do
|
62
|
+
context 'even if ther was not a filter explicitly defined for it' do
|
63
|
+
let(:filters_string) { 'category_uuid=deadbeef1' }
|
64
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.where(category_uuid: 'deadbeef1')
|
65
|
+
end
|
69
66
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
67
|
+
context 'but if it is a field that does not exist in the model' do
|
68
|
+
let(:filters_string) { 'nonexisting=valuehere' }
|
69
|
+
it 'it blows up with the right error' do
|
70
|
+
expect{subject}.to raise_error(/Filtering by nonexisting is not allowed/)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
context 'that maps to a different name' do
|
75
|
+
let(:filters_string) { 'name=Book1'}
|
76
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.where(simple_name: 'Book1')
|
77
|
+
end
|
78
|
+
context 'that is mapped as a nested struct' do
|
79
|
+
let(:filters_string) { 'fake_nested.name=Book1'}
|
80
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.where(simple_name: 'Book1')
|
81
|
+
end
|
82
|
+
context 'passing multiple values' do
|
83
|
+
context 'without fuzzy matching' do
|
84
|
+
let(:filters_string) { 'category_uuid=deadbeef1,deadbeef2' }
|
85
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.where(category_uuid: ['deadbeef1','deadbeef2'])
|
86
|
+
end
|
87
|
+
context 'with fuzzy matching' do
|
88
|
+
let(:filters_string) { 'category_uuid=*deadbeef1,deadbeef2*' }
|
89
|
+
it 'is not supported' do
|
90
|
+
expect{
|
91
|
+
subject
|
92
|
+
}.to raise_error(
|
93
|
+
Praxis::Extensions::AttributeFiltering::MultiMatchWithFuzzyNotAllowedByAdapter,
|
94
|
+
/Please use multiple OR clauses instead/
|
95
|
+
)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
82
99
|
end
|
83
|
-
end
|
84
100
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
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
|
101
|
+
context 'by a field or a related model' do
|
102
|
+
context 'for a belongs_to association' do
|
103
|
+
let(:filters_string) { 'author.name=author2'}
|
104
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.name' => 'author2')
|
105
|
+
end
|
106
|
+
context 'for a has_many association' do
|
107
|
+
let(:filters_string) { 'taggings.label=primary' }
|
108
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:taggings).where('active_taggings.label' => 'primary')
|
109
|
+
end
|
110
|
+
context 'for a has_many through association' do
|
111
|
+
let(:filters_string) { 'tags.name=blue' }
|
112
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:tags).where('active_tags.name' => 'blue')
|
113
|
+
end
|
154
114
|
end
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
115
|
+
|
116
|
+
# NOTE: apparently AR when conditions are build with strings in the where clauses (instead of names, etc)
|
117
|
+
# it decides to parenthesize them, even when there's only 1 condition. Hence the silly parentization of
|
118
|
+
# these SQL fragments here (and others)
|
119
|
+
context 'by using all supported operators' do
|
120
|
+
PREF = Praxis::Extensions::AttributeFiltering::ALIAS_TABLE_PREFIX
|
121
|
+
COMMON_SQL_PREFIX = <<~SQL
|
122
|
+
SELECT "active_books".* FROM "active_books"
|
123
|
+
INNER JOIN
|
124
|
+
"active_authors" "#{PREF}/author" ON "#{PREF}/author"."id" = "active_books"."author_id"
|
125
|
+
SQL
|
126
|
+
context '=' do
|
127
|
+
let(:filters_string) { 'author.id=11'}
|
128
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id = 11')
|
129
|
+
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
130
|
+
WHERE ("#{PREF}/author"."id" = 11)
|
131
|
+
SQL
|
132
|
+
end
|
133
|
+
context '= (with array)' do
|
134
|
+
let(:filters_string) { 'author.id=11,22'}
|
135
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id IN (11,22)')
|
136
|
+
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
137
|
+
WHERE ("#{PREF}/author"."id" IN (11,22))
|
138
|
+
SQL
|
139
|
+
end
|
140
|
+
context '!=' do
|
141
|
+
let(:filters_string) { 'author.id!=11'}
|
142
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id <> 11')
|
143
|
+
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
144
|
+
WHERE ("#{PREF}/author"."id" <> 11)
|
145
|
+
SQL
|
146
|
+
end
|
147
|
+
context '!= (with array)' do
|
148
|
+
let(:filters_string) { 'author.id!=11,888'}
|
149
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id NOT IN (11,888)')
|
150
|
+
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
151
|
+
WHERE ("#{PREF}/author"."id" NOT IN (11,888))
|
152
|
+
SQL
|
153
|
+
end
|
154
|
+
context '>' do
|
155
|
+
let(:filters_string) { 'author.id>1'}
|
156
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id > 1')
|
157
|
+
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
158
|
+
WHERE ("#{PREF}/author"."id" > 1)
|
159
|
+
SQL
|
160
|
+
end
|
161
|
+
context '<' do
|
162
|
+
let(:filters_string) { 'author.id<22'}
|
163
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id < 22')
|
164
|
+
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
165
|
+
WHERE ("#{PREF}/author"."id" < 22)
|
166
|
+
SQL
|
167
|
+
end
|
168
|
+
context '>=' do
|
169
|
+
let(:filters_string) { 'author.id>=22'}
|
170
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id >= 22')
|
166
171
|
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
167
|
-
|
168
|
-
|
172
|
+
WHERE ("#{PREF}/author"."id" >= 22)
|
173
|
+
SQL
|
169
174
|
end
|
170
|
-
context '
|
171
|
-
let(:filters_string) { 'author.
|
172
|
-
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.
|
175
|
+
context '<=' do
|
176
|
+
let(:filters_string) { 'author.id<=22'}
|
177
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id <= 22')
|
173
178
|
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
174
|
-
|
175
|
-
|
179
|
+
WHERE ("#{PREF}/author"."id" <= 22)
|
180
|
+
SQL
|
181
|
+
end
|
182
|
+
context '!' do
|
183
|
+
let(:filters_string) { 'author.id!'}
|
184
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id IS NOT NULL')
|
185
|
+
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
186
|
+
WHERE ("#{PREF}/author"."id" IS NOT NULL)
|
187
|
+
SQL
|
188
|
+
end
|
189
|
+
context '!!' do
|
190
|
+
let(:filters_string) { 'author.name!!'}
|
191
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.name IS NULL')
|
192
|
+
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
193
|
+
WHERE ("#{PREF}/author"."name" IS NULL)
|
194
|
+
SQL
|
195
|
+
end
|
196
|
+
context 'including LIKE fuzzy queries' do
|
197
|
+
context 'LIKE' do
|
198
|
+
let(:filters_string) { 'author.name=author*'}
|
199
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.name LIKE "author%"')
|
200
|
+
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
201
|
+
WHERE ("#{PREF}/author"."name" LIKE 'author%')
|
202
|
+
SQL
|
203
|
+
end
|
204
|
+
context 'NOT LIKE' do
|
205
|
+
let(:filters_string) { 'author.name!=foobar*'}
|
206
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.name NOT LIKE "foobar%"')
|
207
|
+
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
208
|
+
WHERE ("#{PREF}/author"."name" NOT LIKE 'foobar%')
|
209
|
+
SQL
|
210
|
+
end
|
176
211
|
end
|
177
212
|
end
|
178
|
-
end
|
179
213
|
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
214
|
+
context 'with a field mapping using a proc' do
|
215
|
+
let(:filters_string) { 'name_is_not=Book1' }
|
216
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.where.not(simple_name: 'Book1')
|
217
|
+
end
|
184
218
|
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
219
|
+
context 'with a deeply nested chains' do
|
220
|
+
context 'of depth 2' do
|
221
|
+
let(:filters_string) { 'category.books.name=Book2' }
|
222
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(category: :books).where('books_active_categories.simple_name': 'Book2')
|
223
|
+
end
|
224
|
+
context 'multiple conditions on a nested relationship' do
|
225
|
+
let(:filters_string) { 'category.books.taggings.tag_id=1&category.books.taggings.label=primary' }
|
226
|
+
it_behaves_like 'subject_equivalent_to',
|
227
|
+
ActiveBook.joins(category: { books: :taggings }).where('active_taggings.tag_id': 1).where('active_taggings.label': 'primary')
|
228
|
+
it_behaves_like 'subject_matches_sql', <<~SQL
|
229
|
+
SELECT "active_books".* FROM "active_books"
|
230
|
+
INNER JOIN "active_categories" ON "active_categories"."uuid" = "active_books"."category_uuid"
|
231
|
+
INNER JOIN "active_books" "books_active_categories" ON "books_active_categories"."category_uuid" = "active_categories"."uuid"
|
232
|
+
INNER JOIN "active_taggings" "#{PREF}/category/books/taggings" ON "/category/books/taggings"."book_id" = "books_active_categories"."id"
|
233
|
+
WHERE ("#{PREF}/category/books/taggings"."tag_id" = 1)
|
234
|
+
AND ("#{PREF}/category/books/taggings"."label" = 'primary')
|
235
|
+
SQL
|
236
|
+
end
|
237
|
+
context 'that contain multiple joins to the same table' do
|
238
|
+
let(:filters_string) { 'taggings.tag.taggings.tag_id=1' }
|
239
|
+
it_behaves_like 'subject_equivalent_to',
|
240
|
+
ActiveBook.joins(taggings: {tag: :taggings}).where('taggings_active_tags.tag_id=1')
|
241
|
+
end
|
189
242
|
end
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
AND ("#{PREF}/category/books/taggings"."label" = 'primary')
|
201
|
-
SQL
|
243
|
+
|
244
|
+
context 'by multiple fields' do
|
245
|
+
context 'adds the where clauses for the top model if fields belong to it' do
|
246
|
+
let(:filters_string) { 'category_uuid=deadbeef1&name=Book1' }
|
247
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.where(category_uuid: 'deadbeef1', simple_name: 'Book1')
|
248
|
+
end
|
249
|
+
context 'adds multiple where clauses for same nested relationship join (instead of multiple joins with 1 clause each)' do
|
250
|
+
let(:filters_string) { 'taggings.label=primary&taggings.tag_id=2' }
|
251
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:taggings).where('active_taggings.label' => 'primary', 'active_taggings.tag_id' => 2)
|
252
|
+
end
|
202
253
|
end
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
254
|
+
|
255
|
+
context 'uses fully qualified names for conditions (disambiguate fields)' do
|
256
|
+
context 'when we have a join table condition that has the same field' do
|
257
|
+
let(:filters_string) { 'name=Book1&category.books.name=Book3' }
|
258
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(category: :books)
|
259
|
+
.where('simple_name': 'Book1')
|
260
|
+
.where('books_active_categories.simple_name': 'Book3')
|
261
|
+
it_behaves_like 'subject_matches_sql', <<~SQL
|
262
|
+
SELECT "active_books".* FROM "active_books"
|
263
|
+
INNER JOIN "active_categories" ON "active_categories"."uuid" = "active_books"."category_uuid"
|
264
|
+
INNER JOIN "active_books" "#{PREF}/category/books" ON "#{PREF}/category/books"."category_uuid" = "active_categories"."uuid"
|
265
|
+
WHERE ("active_books"."simple_name" = 'Book1')
|
266
|
+
AND ("#{PREF}/category/books"."simple_name" = 'Book3')
|
267
|
+
SQL
|
268
|
+
end
|
269
|
+
|
270
|
+
context 'it qualifis them even if there are no joined tables/conditions at all' do
|
271
|
+
let(:filters_string) { 'id=11'}
|
272
|
+
it_behaves_like 'subject_matches_sql', <<~SQL
|
273
|
+
SELECT "active_books".* FROM "active_books"
|
274
|
+
WHERE ("active_books"."id" = 11)
|
275
|
+
SQL
|
276
|
+
end
|
277
|
+
|
207
278
|
end
|
208
279
|
end
|
209
280
|
|
210
|
-
context '
|
281
|
+
context 'with simple OR conditions' do
|
211
282
|
context 'adds the where clauses for the top model if fields belong to it' do
|
212
|
-
let(:filters_string) { 'category_uuid=deadbeef1
|
213
|
-
it_behaves_like 'subject_equivalent_to', ActiveBook.where(category_uuid: 'deadbeef1'
|
283
|
+
let(:filters_string) { 'category_uuid=deadbeef1|name=Book1' }
|
284
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.where(category_uuid: 'deadbeef1').or(ActiveBook.where(simple_name: 'Book1'))
|
285
|
+
end
|
286
|
+
context 'supports top level parenthesis' do
|
287
|
+
let(:filters_string) { '(category_uuid=deadbeef1|name=Book1)' }
|
288
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.where(category_uuid: 'deadbeef1').or(ActiveBook.where(simple_name: 'Book1'))
|
214
289
|
end
|
215
290
|
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
|
217
|
-
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:taggings).where('active_taggings.label' => 'primary'
|
291
|
+
let(:filters_string) { 'taggings.label=primary|taggings.tag_id=2' }
|
292
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:taggings).where('active_taggings.label' => 'primary')
|
293
|
+
.or(ActiveBook.joins(:taggings).where('active_taggings.tag_id' => 2))
|
218
294
|
end
|
219
295
|
end
|
220
296
|
|
221
|
-
context '
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
INNER JOIN "active_books" "#{PREF}/category/books" ON "#{PREF}/category/books"."category_uuid" = "active_categories"."uuid"
|
231
|
-
WHERE ("#{PREF}/category/books"."simple_name" = 'Book3')
|
232
|
-
AND ("active_books"."simple_name" = 'Book1')
|
233
|
-
SQL
|
234
|
-
end
|
297
|
+
context 'with combined AND and OR conditions' do
|
298
|
+
let(:filters_string) { '(category_uuid=deadbeef1|category_uuid=deadbeef2)&(name=Book1|name=Book2)' }
|
299
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.where(category_uuid: 'deadbeef1').or(ActiveBook.where(category_uuid: 'deadbeef2'))
|
300
|
+
.and(ActiveBook.where(simple_name: 'Book1').or(ActiveBook.where(simple_name: 'Book2')))
|
301
|
+
it_behaves_like 'subject_matches_sql', <<~SQL
|
302
|
+
SELECT "active_books".* FROM "active_books"
|
303
|
+
WHERE ("active_books"."category_uuid" = 'deadbeef1' OR "active_books"."category_uuid" = 'deadbeef2')
|
304
|
+
AND ("active_books"."simple_name" = 'Book1' OR "active_books"."simple_name" = 'Book2')
|
305
|
+
SQL
|
235
306
|
|
236
|
-
context '
|
237
|
-
let(:filters_string) { '
|
307
|
+
context 'adds multiple where clauses for same nested relationship join (instead of multiple joins with 1 clause each)' do
|
308
|
+
let(:filters_string) { 'taggings.label=primary|taggings.tag_id=2' }
|
309
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:taggings).where('active_taggings.label' => 'primary')
|
310
|
+
.or(ActiveBook.joins(:taggings).where('active_taggings.tag_id' => 2))
|
238
311
|
it_behaves_like 'subject_matches_sql', <<~SQL
|
239
312
|
SELECT "active_books".* FROM "active_books"
|
240
|
-
|
241
|
-
|
313
|
+
INNER JOIN "active_taggings" "/taggings" ON "/taggings"."book_id" = "active_books"."id"
|
314
|
+
WHERE ("/taggings"."label" = 'primary' OR "/taggings"."tag_id" = 2)
|
315
|
+
SQL
|
242
316
|
end
|
243
317
|
|
318
|
+
context '3-deep AND and OR conditions' do
|
319
|
+
let(:filters_string) { '(category.name=cat2|(taggings.label=primary&tags.name=red))&category_uuid=deadbeef1' }
|
320
|
+
it_behaves_like('subject_equivalent_to', Proc.new do
|
321
|
+
base=ActiveBook.joins(:category,:taggings,:tags)
|
322
|
+
|
323
|
+
and1_or1 = base.where('category.name': 'cat2')
|
324
|
+
|
325
|
+
and1_or2_and1 = base.where('taggings.label': 'primary')
|
326
|
+
and1_or2_and2 = base.where('tags.name': 'red')
|
327
|
+
and1_or2 = and1_or2_and1.and(and1_or2_and2)
|
328
|
+
|
329
|
+
and1 = and1_or1.or(and1_or2)
|
330
|
+
and2=base.where(category_uuid: 'deadbeef1')
|
331
|
+
|
332
|
+
query = and1.and(and2)
|
333
|
+
end)
|
334
|
+
|
335
|
+
it_behaves_like 'subject_matches_sql', <<~SQL
|
336
|
+
SELECT "active_books".* FROM "active_books"
|
337
|
+
INNER JOIN "active_categories" "/category" ON "/category"."uuid" = "active_books"."category_uuid"
|
338
|
+
INNER JOIN "active_taggings" "/taggings" ON "/taggings"."book_id" = "active_books"."id"
|
339
|
+
INNER JOIN "active_taggings" "taggings_active_books_join" ON "taggings_active_books_join"."book_id" = "active_books"."id"
|
340
|
+
INNER JOIN "active_tags" "/tags" ON "/tags"."id" = "taggings_active_books_join"."tag_id"
|
341
|
+
WHERE ("/category"."name" = 'cat2' OR ("/taggings"."label" = 'primary') AND ("/tags"."name" = 'red')) AND ("active_books"."category_uuid" = 'deadbeef1')
|
342
|
+
SQL
|
343
|
+
end
|
244
344
|
end
|
245
345
|
|
246
346
|
context 'ActiveRecord continues to work as expected (with our patches)' do
|