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.
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