praxis 2.0.pre.10 → 2.0.pre.15

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/.travis.yml +1 -3
  4. data/CHANGELOG.md +26 -0
  5. data/bin/praxis +65 -2
  6. data/lib/praxis/api_definition.rb +8 -4
  7. data/lib/praxis/bootloader_stages/environment.rb +1 -0
  8. data/lib/praxis/collection.rb +11 -0
  9. data/lib/praxis/docs/open_api/response_object.rb +21 -6
  10. data/lib/praxis/docs/open_api_generator.rb +1 -1
  11. data/lib/praxis/extensions/attribute_filtering.rb +14 -1
  12. data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +206 -66
  13. data/lib/praxis/extensions/attribute_filtering/filter_tree_node.rb +3 -2
  14. data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +45 -41
  15. data/lib/praxis/extensions/attribute_filtering/filters_parser.rb +193 -0
  16. data/lib/praxis/extensions/attribute_filtering/sequel_filter_query_builder.rb +20 -8
  17. data/lib/praxis/extensions/pagination.rb +5 -32
  18. data/lib/praxis/mapper/active_model_compat.rb +4 -0
  19. data/lib/praxis/mapper/resource.rb +18 -2
  20. data/lib/praxis/mapper/selector_generator.rb +1 -0
  21. data/lib/praxis/mapper/sequel_compat.rb +7 -0
  22. data/lib/praxis/media_type_identifier.rb +11 -1
  23. data/lib/praxis/plugins/mapper_plugin.rb +22 -13
  24. data/lib/praxis/plugins/pagination_plugin.rb +34 -4
  25. data/lib/praxis/response_definition.rb +46 -66
  26. data/lib/praxis/responses/http.rb +3 -1
  27. data/lib/praxis/tasks/api_docs.rb +4 -1
  28. data/lib/praxis/tasks/routes.rb +6 -6
  29. data/lib/praxis/version.rb +1 -1
  30. data/spec/praxis/action_definition_spec.rb +3 -1
  31. data/spec/praxis/extensions/attribute_filtering/active_record_filter_query_builder_spec.rb +267 -167
  32. data/spec/praxis/extensions/attribute_filtering/filter_tree_node_spec.rb +25 -6
  33. data/spec/praxis/extensions/attribute_filtering/filtering_params_spec.rb +100 -17
  34. data/spec/praxis/extensions/attribute_filtering/filters_parser_spec.rb +148 -0
  35. data/spec/praxis/extensions/field_selection/active_record_query_selector_spec.rb +1 -1
  36. data/spec/praxis/extensions/field_selection/sequel_query_selector_spec.rb +1 -1
  37. data/spec/praxis/extensions/support/spec_resources_active_model.rb +1 -1
  38. data/spec/praxis/mapper/selector_generator_spec.rb +1 -1
  39. data/spec/praxis/media_type_identifier_spec.rb +15 -1
  40. data/spec/praxis/response_definition_spec.rb +37 -129
  41. data/tasks/thor/example.rb +12 -6
  42. data/tasks/thor/model.rb +40 -0
  43. data/tasks/thor/scaffold.rb +117 -0
  44. data/tasks/thor/templates/generator/empty_app/config/environment.rb +1 -0
  45. data/tasks/thor/templates/generator/example_app/Rakefile +9 -2
  46. data/tasks/thor/templates/generator/example_app/app/v1/concerns/controller_base.rb +24 -0
  47. data/tasks/thor/templates/generator/example_app/app/v1/concerns/href.rb +33 -0
  48. data/tasks/thor/templates/generator/example_app/app/v1/controllers/users.rb +2 -2
  49. data/tasks/thor/templates/generator/example_app/app/v1/resources/base.rb +15 -0
  50. data/tasks/thor/templates/generator/example_app/app/v1/resources/user.rb +7 -28
  51. data/tasks/thor/templates/generator/example_app/config.ru +1 -2
  52. data/tasks/thor/templates/generator/example_app/config/environment.rb +3 -2
  53. data/tasks/thor/templates/generator/example_app/db/migrate/20201010101010_create_users_table.rb +3 -2
  54. data/tasks/thor/templates/generator/example_app/db/seeds.rb +6 -0
  55. data/tasks/thor/templates/generator/example_app/design/v1/endpoints/users.rb +4 -4
  56. data/tasks/thor/templates/generator/example_app/design/v1/media_types/user.rb +1 -6
  57. data/tasks/thor/templates/generator/example_app/spec/helpers/database_helper.rb +4 -2
  58. data/tasks/thor/templates/generator/example_app/spec/spec_helper.rb +2 -2
  59. data/tasks/thor/templates/generator/example_app/spec/v1/controllers/users_spec.rb +2 -2
  60. data/tasks/thor/templates/generator/scaffold/design/endpoints/collection.rb +98 -0
  61. data/tasks/thor/templates/generator/scaffold/design/media_types/item.rb +18 -0
  62. data/tasks/thor/templates/generator/scaffold/implementation/controllers/collection.rb +77 -0
  63. data/tasks/thor/templates/generator/scaffold/implementation/resources/base.rb +11 -0
  64. data/tasks/thor/templates/generator/scaffold/implementation/resources/item.rb +45 -0
  65. data/tasks/thor/templates/generator/scaffold/models/active_record.rb +6 -0
  66. data/tasks/thor/templates/generator/scaffold/models/sequel.rb +6 -0
  67. metadata +21 -6
@@ -1,3 +1,3 @@
1
1
  module Praxis
2
- VERSION = '2.0.pre.10'
2
+ VERSION = '2.0.pre.15'
3
3
  end
@@ -40,7 +40,9 @@ describe Praxis::ActionDefinition do
40
40
 
41
41
  media_type media_type
42
42
  location location
43
- headers headers if 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
- # Remove parenthesis as our queries have WHERE clauses using them...
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.gsub(/[()]/,'')
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.query).to eq(base_query)
40
+ expect(instance.instance_variable_get(:@initial_query)).to eq(base_query)
41
41
  expect(instance.model).to eq(base_model)
42
- expect(instance.attr_to_column).to eq(filters_map)
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 '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
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
- 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')
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
- 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
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
- 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%"')
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
- WHERE "#{PREF}/author"."name" LIKE 'author%'
168
- SQL
172
+ WHERE ("#{PREF}/author"."id" >= 22)
173
+ SQL
169
174
  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%"')
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
- WHERE "#{PREF}/author"."name" NOT LIKE 'foobar%'
175
- SQL
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
- 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
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
- 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')
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
- 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
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
- 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')
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 'by multiple fields' do
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&name=Book1' }
213
- it_behaves_like 'subject_equivalent_to', ActiveBook.where(category_uuid: 'deadbeef1', simple_name: 'Book1')
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&taggings.tag_id=2' }
217
- it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:taggings).where('active_taggings.label' => 'primary', 'active_taggings.tag_id' => 2)
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 '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
- let(:filters_string) { 'name=Book1&category.books.name=Book3' }
224
- it_behaves_like 'subject_equivalent_to', ActiveBook.joins(category: :books)
225
- .where('simple_name': 'Book1')
226
- .where('books_active_categories.simple_name': 'Book3')
227
- it_behaves_like 'subject_matches_sql', <<~SQL
228
- SELECT "active_books".* FROM "active_books"
229
- INNER JOIN "active_categories" ON "active_categories"."uuid" = "active_books"."category_uuid"
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 'it qualifis them even if there are no joined tables/conditions at all' do
237
- let(:filters_string) { 'id=11'}
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
- WHERE "active_books"."id" = 11
241
- SQL
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