praxis 2.0.pre.11 → 2.0.pre.16
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/CHANGELOG.md +22 -0
- data/bin/praxis +6 -0
- data/lib/praxis/api_definition.rb +8 -4
- data/lib/praxis/collection.rb +11 -0
- data/lib/praxis/docs/open_api/response_object.rb +21 -6
- data/lib/praxis/extensions/attribute_filtering.rb +14 -1
- data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +154 -63
- data/lib/praxis/extensions/attribute_filtering/filter_tree_node.rb +3 -2
- data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +46 -43
- data/lib/praxis/extensions/attribute_filtering/filters_parser.rb +193 -0
- data/lib/praxis/mapper/resource.rb +2 -2
- data/lib/praxis/media_type_identifier.rb +11 -1
- data/lib/praxis/response_definition.rb +46 -66
- data/lib/praxis/responses/http.rb +3 -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 +259 -172
- data/spec/praxis/extensions/attribute_filtering/filter_tree_node_spec.rb +25 -6
- data/spec/praxis/extensions/attribute_filtering/filtering_params_spec.rb +117 -19
- data/spec/praxis/extensions/attribute_filtering/filters_parser_spec.rb +148 -0
- data/spec/praxis/mapper/resource_spec.rb +3 -3
- data/spec/praxis/media_type_identifier_spec.rb +15 -1
- data/spec/praxis/response_definition_spec.rb +37 -129
- data/tasks/thor/templates/generator/example_app/app/v1/concerns/href.rb +33 -0
- data/tasks/thor/templates/generator/example_app/app/v1/resources/base.rb +4 -0
- data/tasks/thor/templates/generator/example_app/config/environment.rb +1 -1
- data/tasks/thor/templates/generator/scaffold/implementation/resources/item.rb +2 -2
- metadata +9 -6
data/lib/praxis/tasks/routes.rb
CHANGED
@@ -7,14 +7,14 @@ namespace :praxis do
|
|
7
7
|
table = Terminal::Table.new title: "Routes",
|
8
8
|
headings: [
|
9
9
|
"Version", "Path", "Verb",
|
10
|
-
"
|
10
|
+
"Endpoint", "Action", "Implementation", "Options"
|
11
11
|
]
|
12
12
|
|
13
13
|
rows = []
|
14
|
-
Praxis::Application.instance.endpoint_definitions.each do |
|
15
|
-
|
14
|
+
Praxis::Application.instance.endpoint_definitions.each do |endpoint_definition|
|
15
|
+
endpoint_definition.actions.each do |name, action|
|
16
16
|
method = begin
|
17
|
-
m =
|
17
|
+
m = endpoint_definition.controller.instance_method(name)
|
18
18
|
rescue
|
19
19
|
nil
|
20
20
|
end
|
@@ -22,13 +22,13 @@ namespace :praxis do
|
|
22
22
|
method_name = method ? "#{method.owner.name}##{method.name}" : 'n/a'
|
23
23
|
|
24
24
|
row = {
|
25
|
-
resource:
|
25
|
+
resource: endpoint_definition.name,
|
26
26
|
action: name,
|
27
27
|
implementation: method_name,
|
28
28
|
}
|
29
29
|
|
30
30
|
unless action.route
|
31
|
-
warn "Warning: No routes defined for #{
|
31
|
+
warn "Warning: No routes defined for #{endpoint_definition.name}##{name}."
|
32
32
|
rows << row
|
33
33
|
else
|
34
34
|
route = action.route
|
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,7 +37,7 @@ 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
42
|
expect(instance.filters_map).to eq(filters_map)
|
43
43
|
end
|
@@ -52,208 +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
|
-
it_behaves_like 'subject_equivalent_to', ActiveBook.where(category_uuid: 'deadbeef1')
|
59
|
-
end
|
60
|
-
context 'same-name filter mapping works' do
|
61
|
-
context 'even if ther was not a filter explicitly defined for it' do
|
55
|
+
context 'with flat AND conditions' do
|
56
|
+
context 'by a simple field' do
|
57
|
+
context 'that maps to the same name' do
|
62
58
|
let(:filters_string) { 'category_uuid=deadbeef1' }
|
63
59
|
it_behaves_like 'subject_equivalent_to', ActiveBook.where(category_uuid: 'deadbeef1')
|
64
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
|
65
66
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
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
|
70
72
|
end
|
71
73
|
end
|
72
|
-
end
|
73
74
|
context 'that maps to a different name' do
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
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
|
80
99
|
end
|
81
|
-
end
|
82
100
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
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
|
95
114
|
end
|
96
|
-
end
|
97
115
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
end
|
161
|
-
context '!' do
|
162
|
-
let(:filters_string) { 'author.id!'}
|
163
|
-
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id IS NOT NULL')
|
164
|
-
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
165
|
-
WHERE "#{PREF}/author"."id" IS NOT NULL
|
166
|
-
SQL
|
167
|
-
end
|
168
|
-
context '!!' do
|
169
|
-
let(:filters_string) { 'author.name!!'}
|
170
|
-
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.name IS NULL')
|
171
|
-
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
172
|
-
WHERE "#{PREF}/author"."name" IS NULL
|
173
|
-
SQL
|
174
|
-
end
|
175
|
-
context 'including LIKE fuzzy queries' do
|
176
|
-
context 'LIKE' do
|
177
|
-
let(:filters_string) { 'author.name=author*'}
|
178
|
-
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.name LIKE "author%"')
|
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')
|
171
|
+
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
172
|
+
WHERE ("#{PREF}/author"."id" >= 22)
|
173
|
+
SQL
|
174
|
+
end
|
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')
|
179
178
|
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
180
|
-
|
181
|
-
|
179
|
+
WHERE ("#{PREF}/author"."id" <= 22)
|
180
|
+
SQL
|
182
181
|
end
|
183
|
-
context '
|
184
|
-
let(:filters_string) { 'author.
|
185
|
-
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.
|
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')
|
186
185
|
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
187
|
-
|
188
|
-
|
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
|
189
211
|
end
|
190
212
|
end
|
191
|
-
end
|
192
213
|
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
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
|
197
218
|
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
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
|
202
242
|
end
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
AND ("#{PREF}/category/books/taggings"."label" = 'primary')
|
214
|
-
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
|
215
253
|
end
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
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
|
+
|
220
278
|
end
|
221
279
|
end
|
222
280
|
|
223
|
-
context '
|
281
|
+
context 'with simple OR conditions' do
|
224
282
|
context 'adds the where clauses for the top model if fields belong to it' do
|
225
|
-
let(:filters_string) { 'category_uuid=deadbeef1
|
226
|
-
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'))
|
227
289
|
end
|
228
290
|
context 'adds multiple where clauses for same nested relationship join (instead of multiple joins with 1 clause each)' do
|
229
|
-
let(:filters_string) { 'taggings.label=primary
|
230
|
-
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))
|
231
294
|
end
|
232
295
|
end
|
233
296
|
|
234
|
-
context '
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
INNER JOIN "active_books" "#{PREF}/category/books" ON "#{PREF}/category/books"."category_uuid" = "active_categories"."uuid"
|
244
|
-
WHERE ("#{PREF}/category/books"."simple_name" = 'Book3')
|
245
|
-
AND ("active_books"."simple_name" = 'Book1')
|
246
|
-
SQL
|
247
|
-
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
|
248
306
|
|
249
|
-
context '
|
250
|
-
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))
|
251
311
|
it_behaves_like 'subject_matches_sql', <<~SQL
|
252
312
|
SELECT "active_books".* FROM "active_books"
|
253
|
-
|
254
|
-
|
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
|
255
316
|
end
|
256
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
|
257
344
|
end
|
258
345
|
|
259
346
|
context 'ActiveRecord continues to work as expected (with our patches)' do
|