praxis 2.0.pre.14 → 2.0.pre.18
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/CHANGELOG.md +13 -0
- data/bin/praxis +6 -0
- data/lib/praxis/action_definition.rb +2 -2
- data/lib/praxis/api_definition.rb +8 -4
- data/lib/praxis/blueprint.rb +22 -7
- data/lib/praxis/collection.rb +11 -0
- data/lib/praxis/dispatcher.rb +3 -3
- data/lib/praxis/docs/open_api/response_object.rb +21 -6
- data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +63 -16
- data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +1 -2
- data/lib/praxis/mapper/resource.rb +2 -2
- data/lib/praxis/mapper/selector_generator.rb +2 -2
- data/lib/praxis/media_type_identifier.rb +11 -1
- data/lib/praxis/request.rb +5 -0
- data/lib/praxis/request_stages/validate_params_and_headers.rb +0 -6
- data/lib/praxis/request_stages/validate_payload.rb +0 -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/types/multipart_array.rb +14 -5
- data/lib/praxis/version.rb +1 -1
- data/praxis.gemspec +1 -1
- data/spec/praxis/action_definition_spec.rb +6 -3
- data/spec/praxis/extensions/attribute_filtering/active_record_filter_query_builder_spec.rb +92 -34
- data/spec/praxis/extensions/attribute_filtering/filtering_params_spec.rb +17 -2
- data/spec/praxis/extensions/support/spec_resources_active_model.rb +2 -0
- data/spec/praxis/mapper/resource_spec.rb +3 -3
- data/spec/praxis/mapper/selector_generator_spec.rb +34 -0
- data/spec/praxis/media_type_identifier_spec.rb +15 -1
- data/spec/praxis/request_spec.rb +3 -3
- data/spec/praxis/response_definition_spec.rb +37 -129
- data/spec/praxis/trait_spec.rb +3 -2
- data/spec/spec_app/design/media_types/instance.rb +1 -1
- data/spec/spec_app/design/resources/instances.rb +2 -2
- data/spec/spec_helper.rb +1 -0
- data/spec/support/spec_blueprints.rb +3 -3
- data/spec/support/spec_resources.rb +4 -0
- 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 -8
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
|
@@ -346,6 +346,10 @@ module Praxis
|
|
346
346
|
end
|
347
347
|
end
|
348
348
|
|
349
|
+
def part?(name)
|
350
|
+
self.any?{ |i| i.name == name }
|
351
|
+
end
|
352
|
+
|
349
353
|
def validate(context=Attributor::DEFAULT_ROOT_CONTEXT)
|
350
354
|
errors = self.each_with_index.each_with_object([]) do |(part, idx), errors|
|
351
355
|
sub_context = if part.name
|
@@ -359,13 +363,18 @@ module Praxis
|
|
359
363
|
|
360
364
|
self.class.attributes.each do |name, attribute|
|
361
365
|
payload_attribute = attribute.options[:payload_attribute]
|
362
|
-
next unless payload_attribute.options[:required]
|
363
|
-
next if self.part(name)
|
364
366
|
|
365
|
-
|
366
|
-
|
367
|
+
if !self.part?(name)
|
368
|
+
if payload_attribute.options[:required]
|
369
|
+
sub_context = self.class.generate_subcontext(context, name)
|
370
|
+
errors.push "Attribute #{Attributor.humanize_context(sub_context)} is required"
|
371
|
+
end
|
372
|
+
# Return, don't bother checking nullability as it hasn't been provided
|
373
|
+
elsif !self.part(name) && !Attribute.nullable_attribute?(payload_attribute.options)
|
374
|
+
sub_context = self.class.generate_subcontext(context, name)
|
375
|
+
errors.push "Attribute #{Attributor.humanize_context(sub_context)} is not nullable"
|
376
|
+
end
|
367
377
|
end
|
368
|
-
|
369
378
|
errors
|
370
379
|
end
|
371
380
|
|
data/lib/praxis/version.rb
CHANGED
data/praxis.gemspec
CHANGED
@@ -24,7 +24,7 @@ Gem::Specification.new do |spec|
|
|
24
24
|
spec.add_dependency 'mustermann', '>=1.1', '<=2'
|
25
25
|
spec.add_dependency 'activesupport', '>= 3'
|
26
26
|
spec.add_dependency 'mime', '~> 0'
|
27
|
-
spec.add_dependency 'attributor', '>=
|
27
|
+
spec.add_dependency 'attributor', '>= 6.0'
|
28
28
|
spec.add_dependency 'thor'
|
29
29
|
spec.add_dependency 'terminal-table', '~> 1.4'
|
30
30
|
|
@@ -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
|
@@ -151,15 +153,16 @@ describe Praxis::ActionDefinition do
|
|
151
153
|
it 'includes the requirements in the param struct type' do
|
152
154
|
errors = action.params.load(value).validate
|
153
155
|
expect(errors).to have(1).item
|
154
|
-
expect(errors.first).to match(
|
156
|
+
expect(errors.first).to match('Attribute $.key(:one) is required.')
|
155
157
|
end
|
156
158
|
end
|
157
159
|
|
158
160
|
end
|
159
161
|
|
160
162
|
describe '#payload' do
|
161
|
-
it 'defaults to being required if omitted' do
|
163
|
+
it 'defaults to being required and non nullable if omitted' do
|
162
164
|
expect(subject.payload.options[:required]).to be(true)
|
165
|
+
expect(subject.payload.options[:null]).to be(false)
|
163
166
|
end
|
164
167
|
|
165
168
|
|
@@ -111,6 +111,48 @@ describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
|
|
111
111
|
let(:filters_string) { 'tags.name=blue' }
|
112
112
|
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:tags).where('active_tags.name' => 'blue')
|
113
113
|
end
|
114
|
+
|
115
|
+
context 'by just an association filter condition' do
|
116
|
+
context 'for a belongs_to association with NO ROWS' do
|
117
|
+
let(:filters_string) { 'category!!'}
|
118
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.where.missing(:category)
|
119
|
+
end
|
120
|
+
|
121
|
+
context 'for a direct has_many association asking for missing rows' do
|
122
|
+
let(:filters_string) { 'primary_tags!!' }
|
123
|
+
it_behaves_like 'subject_equivalent_to',
|
124
|
+
ActiveBook.where.missing(:primary_tags)
|
125
|
+
end
|
126
|
+
context 'for a direct has_many association asking for non-missing rows' do
|
127
|
+
let(:filters_string) { 'primary_tags!' }
|
128
|
+
it_behaves_like 'subject_equivalent_to',
|
129
|
+
ActiveBook.left_outer_joins(:primary_tags).where.not('primary_tags.id' => nil)
|
130
|
+
end
|
131
|
+
|
132
|
+
context 'for a has_many through association with NO ROWS' do
|
133
|
+
let(:filters_string) { 'tags!!' }
|
134
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.where.missing(:tags)
|
135
|
+
end
|
136
|
+
|
137
|
+
context 'for a has_many through association with SOME ROWS' do
|
138
|
+
let(:filters_string) { 'tags!' }
|
139
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.left_outer_joins(:tags).where.not('tags.id' => nil)
|
140
|
+
end
|
141
|
+
|
142
|
+
context 'for a 3 levels deep has_many association with NO ROWS' do
|
143
|
+
let(:filters_string) { 'category.books.taggings!!' }
|
144
|
+
it_behaves_like 'subject_equivalent_to',
|
145
|
+
ActiveBook.left_outer_joins(category: { books: :taggings }).where('category.books.taggings.id' => nil)
|
146
|
+
end
|
147
|
+
|
148
|
+
context 'for a 3 levels deep has_many association WITH SIME ROWS' do
|
149
|
+
let(:filters_string) { 'category.books.taggings!' }
|
150
|
+
it_behaves_like 'subject_equivalent_to',
|
151
|
+
ActiveBook.left_outer_joins(category: { books: :taggings }).where.not('category.books.taggings.id' => nil)
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|
155
|
+
|
114
156
|
end
|
115
157
|
|
116
158
|
# NOTE: apparently AR when conditions are build with strings in the where clauses (instead of names, etc)
|
@@ -120,77 +162,77 @@ describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
|
|
120
162
|
PREF = Praxis::Extensions::AttributeFiltering::ALIAS_TABLE_PREFIX
|
121
163
|
COMMON_SQL_PREFIX = <<~SQL
|
122
164
|
SELECT "active_books".* FROM "active_books"
|
123
|
-
|
165
|
+
LEFT OUTER JOIN
|
124
166
|
"active_authors" "#{PREF}/author" ON "#{PREF}/author"."id" = "active_books"."author_id"
|
125
167
|
SQL
|
126
168
|
context '=' do
|
127
169
|
let(:filters_string) { 'author.id=11'}
|
128
170
|
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id = 11')
|
129
171
|
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
130
|
-
WHERE ("#{PREF}/author"."id" = 11)
|
172
|
+
WHERE ("#{PREF}/author"."id" IS NOT NULL) AND ("#{PREF}/author"."id" = 11)
|
131
173
|
SQL
|
132
174
|
end
|
133
175
|
context '= (with array)' do
|
134
176
|
let(:filters_string) { 'author.id=11,22'}
|
135
177
|
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id IN (11,22)')
|
136
178
|
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
137
|
-
WHERE ("#{PREF}/author"."id" IN (11,22))
|
179
|
+
WHERE ("#{PREF}/author"."id" IS NOT NULL) AND ("#{PREF}/author"."id" IN (11,22))
|
138
180
|
SQL
|
139
181
|
end
|
140
182
|
context '!=' do
|
141
183
|
let(:filters_string) { 'author.id!=11'}
|
142
184
|
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id <> 11')
|
143
185
|
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
144
|
-
WHERE ("#{PREF}/author"."id" <> 11)
|
186
|
+
WHERE ("#{PREF}/author"."id" IS NOT NULL) AND ("#{PREF}/author"."id" <> 11)
|
145
187
|
SQL
|
146
188
|
end
|
147
189
|
context '!= (with array)' do
|
148
190
|
let(:filters_string) { 'author.id!=11,888'}
|
149
191
|
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id NOT IN (11,888)')
|
150
192
|
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
151
|
-
WHERE ("#{PREF}/author"."id" NOT IN (11,888))
|
193
|
+
WHERE ("#{PREF}/author"."id" IS NOT NULL) AND ("#{PREF}/author"."id" NOT IN (11,888))
|
152
194
|
SQL
|
153
195
|
end
|
154
196
|
context '>' do
|
155
197
|
let(:filters_string) { 'author.id>1'}
|
156
198
|
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id > 1')
|
157
199
|
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
158
|
-
WHERE ("#{PREF}/author"."id" > 1)
|
200
|
+
WHERE ("#{PREF}/author"."id" IS NOT NULL) AND ("#{PREF}/author"."id" > 1)
|
159
201
|
SQL
|
160
202
|
end
|
161
203
|
context '<' do
|
162
204
|
let(:filters_string) { 'author.id<22'}
|
163
205
|
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id < 22')
|
164
206
|
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
165
|
-
WHERE ("#{PREF}/author"."id" < 22)
|
207
|
+
WHERE ("#{PREF}/author"."id" IS NOT NULL) AND ("#{PREF}/author"."id" < 22)
|
166
208
|
SQL
|
167
209
|
end
|
168
210
|
context '>=' do
|
169
211
|
let(:filters_string) { 'author.id>=22'}
|
170
212
|
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id >= 22')
|
171
213
|
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
172
|
-
WHERE ("#{PREF}/author"."id" >= 22)
|
214
|
+
WHERE ("#{PREF}/author"."id" IS NOT NULL) AND ("#{PREF}/author"."id" >= 22)
|
173
215
|
SQL
|
174
216
|
end
|
175
217
|
context '<=' do
|
176
218
|
let(:filters_string) { 'author.id<=22'}
|
177
219
|
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id <= 22')
|
178
220
|
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
179
|
-
WHERE ("#{PREF}/author"."id" <= 22)
|
221
|
+
WHERE ("#{PREF}/author"."id" IS NOT NULL) AND ("#{PREF}/author"."id" <= 22)
|
180
222
|
SQL
|
181
223
|
end
|
182
224
|
context '!' do
|
183
225
|
let(:filters_string) { 'author.id!'}
|
184
226
|
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.id IS NOT NULL')
|
185
227
|
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
186
|
-
WHERE ("#{PREF}/author"."id" IS NOT NULL)
|
228
|
+
WHERE ("#{PREF}/author"."id" IS NOT NULL) AND ("#{PREF}/author"."id" IS NOT NULL)
|
187
229
|
SQL
|
188
230
|
end
|
189
231
|
context '!!' do
|
190
232
|
let(:filters_string) { 'author.name!!'}
|
191
233
|
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.name IS NULL')
|
192
234
|
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
193
|
-
WHERE ("#{PREF}/author"."name" IS NULL)
|
235
|
+
WHERE ("#{PREF}/author"."id" IS NOT NULL) AND ("#{PREF}/author"."name" IS NULL)
|
194
236
|
SQL
|
195
237
|
end
|
196
238
|
context 'including LIKE fuzzy queries' do
|
@@ -198,14 +240,14 @@ describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
|
|
198
240
|
let(:filters_string) { 'author.name=author*'}
|
199
241
|
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.name LIKE "author%"')
|
200
242
|
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
201
|
-
WHERE ("#{PREF}/author"."name" LIKE 'author%')
|
243
|
+
WHERE ("#{PREF}/author"."id" IS NOT NULL) AND ("#{PREF}/author"."name" LIKE 'author%')
|
202
244
|
SQL
|
203
245
|
end
|
204
246
|
context 'NOT LIKE' do
|
205
247
|
let(:filters_string) { 'author.name!=foobar*'}
|
206
248
|
it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:author).where('active_authors.name NOT LIKE "foobar%"')
|
207
249
|
it_behaves_like 'subject_matches_sql', COMMON_SQL_PREFIX + <<~SQL
|
208
|
-
WHERE ("#{PREF}/author"."name" NOT LIKE 'foobar%')
|
250
|
+
WHERE ("#{PREF}/author"."id" IS NOT NULL) AND ("#{PREF}/author"."name" NOT LIKE 'foobar%')
|
209
251
|
SQL
|
210
252
|
end
|
211
253
|
end
|
@@ -227,10 +269,10 @@ describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
|
|
227
269
|
ActiveBook.joins(category: { books: :taggings }).where('active_taggings.tag_id': 1).where('active_taggings.label': 'primary')
|
228
270
|
it_behaves_like 'subject_matches_sql', <<~SQL
|
229
271
|
SELECT "active_books".* FROM "active_books"
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
WHERE ("#{PREF}/category/books/taggings"."tag_id" = 1)
|
272
|
+
LEFT OUTER JOIN "active_categories" ON "active_categories"."uuid" = "active_books"."category_uuid"
|
273
|
+
LEFT OUTER JOIN "active_books" "books_active_categories" ON "books_active_categories"."category_uuid" = "active_categories"."uuid"
|
274
|
+
LEFT OUTER JOIN "active_taggings" "#{PREF}/category/books/taggings" ON "/category/books/taggings"."book_id" = "books_active_categories"."id"
|
275
|
+
WHERE ("#{PREF}/category/books/taggings"."id" IS NOT NULL) AND ("#{PREF}/category/books/taggings"."tag_id" = 1)
|
234
276
|
AND ("#{PREF}/category/books/taggings"."label" = 'primary')
|
235
277
|
SQL
|
236
278
|
end
|
@@ -260,14 +302,14 @@ describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
|
|
260
302
|
.where('books_active_categories.simple_name': 'Book3')
|
261
303
|
it_behaves_like 'subject_matches_sql', <<~SQL
|
262
304
|
SELECT "active_books".* FROM "active_books"
|
263
|
-
|
264
|
-
|
305
|
+
LEFT OUTER JOIN "active_categories" ON "active_categories"."uuid" = "active_books"."category_uuid"
|
306
|
+
LEFT OUTER JOIN "active_books" "#{PREF}/category/books" ON "#{PREF}/category/books"."category_uuid" = "active_categories"."uuid"
|
265
307
|
WHERE ("active_books"."simple_name" = 'Book1')
|
266
|
-
AND ("#{PREF}/category/books"."simple_name" = 'Book3')
|
308
|
+
AND ("#{PREF}/category/books"."id" IS NOT NULL) AND ("#{PREF}/category/books"."simple_name" = 'Book3')
|
267
309
|
SQL
|
268
310
|
end
|
269
311
|
|
270
|
-
context 'it
|
312
|
+
context 'it qualifies them even if there are no joined tables/conditions at all' do
|
271
313
|
let(:filters_string) { 'id=11'}
|
272
314
|
it_behaves_like 'subject_matches_sql', <<~SQL
|
273
315
|
SELECT "active_books".* FROM "active_books"
|
@@ -310,20 +352,31 @@ describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
|
|
310
352
|
.or(ActiveBook.joins(:taggings).where('active_taggings.tag_id' => 2))
|
311
353
|
it_behaves_like 'subject_matches_sql', <<~SQL
|
312
354
|
SELECT "active_books".* FROM "active_books"
|
313
|
-
|
314
|
-
WHERE ("/taggings"."label" = 'primary' OR "/taggings"."tag_id" = 2)
|
355
|
+
LEFT OUTER JOIN "active_taggings" "/taggings" ON "/taggings"."book_id" = "active_books"."id"
|
356
|
+
WHERE ("/taggings"."id" IS NOT NULL) AND ("/taggings"."label" = 'primary' OR "/taggings"."tag_id" = 2)
|
357
|
+
SQL
|
358
|
+
end
|
359
|
+
|
360
|
+
context 'works well with ORs at a parent table along with joined associations with no rows' do
|
361
|
+
let(:filters_string) { 'name=Book1005|category!!' }
|
362
|
+
it_behaves_like 'subject_equivalent_to', ActiveBook.where.missing(:category)
|
363
|
+
.or(ActiveBook.where.missing(:category).where(simple_name: "Book1005"))
|
364
|
+
it_behaves_like 'subject_matches_sql', <<~SQL
|
365
|
+
SELECT "active_books".* FROM "active_books"
|
366
|
+
LEFT OUTER JOIN "active_categories" "/category" ON "/category"."uuid" = "active_books"."category_uuid"
|
367
|
+
WHERE ("active_books"."simple_name" = 'Book1005' OR "/category"."uuid" IS NULL)
|
315
368
|
SQL
|
316
369
|
end
|
317
370
|
|
318
371
|
context '3-deep AND and OR conditions' do
|
319
372
|
let(:filters_string) { '(category.name=cat2|(taggings.label=primary&tags.name=red))&category_uuid=deadbeef1' }
|
320
373
|
it_behaves_like('subject_equivalent_to', Proc.new do
|
321
|
-
base=ActiveBook.
|
374
|
+
base=ActiveBook.left_outer_joins(:category,:taggings,:tags)
|
322
375
|
|
323
|
-
and1_or1 = base.where('category.name': 'cat2')
|
376
|
+
and1_or1 = base.where('category.name': 'cat2').where.not('category.uuid': nil)
|
324
377
|
|
325
|
-
and1_or2_and1 = base.where('taggings.label': 'primary')
|
326
|
-
and1_or2_and2 = base.where('tags.name': 'red')
|
378
|
+
and1_or2_and1 = base.where('taggings.label': 'primary').where.not('taggings.id': nil)
|
379
|
+
and1_or2_and2 = base.where('tags.name': 'red').where.not('tags.id': nil)
|
327
380
|
and1_or2 = and1_or2_and1.and(and1_or2_and2)
|
328
381
|
|
329
382
|
and1 = and1_or1.or(and1_or2)
|
@@ -334,11 +387,16 @@ describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
|
|
334
387
|
|
335
388
|
it_behaves_like 'subject_matches_sql', <<~SQL
|
336
389
|
SELECT "active_books".* FROM "active_books"
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
390
|
+
LEFT OUTER JOIN "active_categories" "/category" ON "/category"."uuid" = "active_books"."category_uuid"
|
391
|
+
LEFT OUTER JOIN "active_taggings" "/taggings" ON "/taggings"."book_id" = "active_books"."id"
|
392
|
+
LEFT OUTER JOIN "active_tags" "/tags" ON "/tags"."id" = "/taggings"."tag_id"
|
393
|
+
WHERE (("/category"."uuid" IS NOT NULL)
|
394
|
+
AND ("/category"."name" = 'cat2')
|
395
|
+
OR ("/taggings"."id" IS NOT NULL)
|
396
|
+
AND ("/taggings"."label" = 'primary')
|
397
|
+
AND ("/tags"."id" IS NOT NULL)
|
398
|
+
AND ("/tags"."name" = 'red'))
|
399
|
+
AND ("active_books"."category_uuid" = 'deadbeef1')
|
342
400
|
SQL
|
343
401
|
end
|
344
402
|
end
|
@@ -391,9 +449,9 @@ describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
|
|
391
449
|
# This is slightly incorrect in AR 6.1+ (since the picked aliases for active_taggings tables vary)
|
392
450
|
# it_behaves_like 'subject_matches_sql', <<~SQL
|
393
451
|
# SELECT "active_books".* FROM "active_books"
|
394
|
-
#
|
452
|
+
# LEFT OUTER JOIN "active_taggings" ON "active_taggings"."label" = 'primary'
|
395
453
|
# AND "active_taggings"."book_id" = "active_books"."id"
|
396
|
-
#
|
454
|
+
# LEFT OUTER JOIN "active_tags" "/primary_tags" ON "/primary_tags"."id" = "active_taggings"."tag_id"
|
397
455
|
# WHERE ("/primary_tags"."name" = 'blue')
|
398
456
|
# SQL
|
399
457
|
end
|
@@ -161,9 +161,9 @@ describe Praxis::Extensions::AttributeFiltering::FilteringParams do
|
|
161
161
|
# construct it propertly by applying the block. Seems easier than creating the type alone, and
|
162
162
|
# then manually apply the block
|
163
163
|
Attributor::Attribute.new(described_class.for(Post)) do
|
164
|
-
filter 'id', using: ['=', '!=']
|
164
|
+
filter 'id', using: ['=', '!=', '!']
|
165
165
|
filter 'title', using: ['=', '!='], fuzzy: true
|
166
|
-
filter 'content', using: ['=', '!=']
|
166
|
+
filter 'content', using: ['=', '!=', '!']
|
167
167
|
end.type
|
168
168
|
end
|
169
169
|
let(:loaded_params) { filtering_params_type.load(filters_string) }
|
@@ -194,6 +194,21 @@ describe Praxis::Extensions::AttributeFiltering::FilteringParams do
|
|
194
194
|
end
|
195
195
|
end
|
196
196
|
end
|
197
|
+
|
198
|
+
context 'non-valued operators' do
|
199
|
+
context 'for string typed fields' do
|
200
|
+
let(:filters_string) { 'content!'}
|
201
|
+
it 'validates properly' do
|
202
|
+
expect(subject).to be_empty
|
203
|
+
end
|
204
|
+
end
|
205
|
+
context 'for non-string typed fields' do
|
206
|
+
let(:filters_string) { 'id!'}
|
207
|
+
it 'validates properly' do
|
208
|
+
expect(subject).to be_empty
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
197
212
|
context 'fuzzy matches' do
|
198
213
|
context 'when allowed' do
|
199
214
|
context 'given a fuzzy string' do
|
@@ -120,6 +120,8 @@ class ActiveBookResource < ActiveBaseResource
|
|
120
120
|
'category.books.name': 'category.books.simple_name',
|
121
121
|
'category.books.taggings.tag_id': 'category.books.taggings.tag_id',
|
122
122
|
'category.books.taggings.label': 'category.books.taggings.label',
|
123
|
+
'primary_tags': 'primary_tags',
|
124
|
+
'category.books.taggings': 'category.books.taggings',
|
123
125
|
)
|
124
126
|
# Forces to add an extra column (added_column)...and yet another (author_id) that will serve
|
125
127
|
# to check that if that's already automatically added due to an association, it won't interfere or duplicate
|
@@ -14,15 +14,15 @@ describe Praxis::Mapper::Resource do
|
|
14
14
|
subject(:properties) { resource.properties }
|
15
15
|
|
16
16
|
it 'includes directly-set properties' do
|
17
|
-
expect(properties[:other_resource]).to eq(dependencies: [:other_model])
|
17
|
+
expect(properties[:other_resource]).to eq(dependencies: [:other_model], through: nil)
|
18
18
|
end
|
19
19
|
|
20
20
|
it 'inherits from a superclass' do
|
21
|
-
expect(properties[:href]).to eq(dependencies: [:id])
|
21
|
+
expect(properties[:href]).to eq(dependencies: [:id], through: nil)
|
22
22
|
end
|
23
23
|
|
24
24
|
it 'properly overrides a property from the parent' do
|
25
|
-
expect(properties[:name]).to eq(dependencies: [:simple_name])
|
25
|
+
expect(properties[:name]).to eq(dependencies: [:simple_name], through: nil)
|
26
26
|
end
|
27
27
|
end
|
28
28
|
end
|
@@ -320,6 +320,40 @@ describe Praxis::Mapper::SelectorGenerator do
|
|
320
320
|
it_behaves_like 'a proper selector'
|
321
321
|
end
|
322
322
|
|
323
|
+
context 'that are several attriutes deep' do
|
324
|
+
let(:fields) { { deep_nested_deps: true } }
|
325
|
+
let(:selectors) do
|
326
|
+
{
|
327
|
+
model: SimpleModel,
|
328
|
+
columns: [:parent_id],
|
329
|
+
tracks: {
|
330
|
+
parent: {
|
331
|
+
model: ParentModel,
|
332
|
+
columns: [:id], # No FKs in the source model for one_to_many
|
333
|
+
tracks: {
|
334
|
+
simple_children: {
|
335
|
+
columns: [:parent_id, :other_model_id],
|
336
|
+
model: SimpleModel,
|
337
|
+
tracks: {
|
338
|
+
other_model: {
|
339
|
+
model: OtherModel,
|
340
|
+
columns: [:id, :parent_id],
|
341
|
+
tracks: {
|
342
|
+
parent: {
|
343
|
+
model: ParentModel,
|
344
|
+
columns: [:id, :simple_name, :other_attribute]
|
345
|
+
}
|
346
|
+
}
|
347
|
+
}
|
348
|
+
}
|
349
|
+
}
|
350
|
+
}
|
351
|
+
}
|
352
|
+
}
|
353
|
+
}
|
354
|
+
end
|
355
|
+
it_behaves_like 'a proper selector'
|
356
|
+
end
|
323
357
|
end
|
324
358
|
end
|
325
359
|
end
|
@@ -218,7 +218,21 @@ describe Praxis::MediaTypeIdentifier do
|
|
218
218
|
|
219
219
|
it 'replaces suffix and parameters and adds new ones' do
|
220
220
|
expect(complex_subject + 'json; nuts=false; cherry=true').to \
|
221
|
-
|
221
|
+
eq(described_class.new('application/vnd.icecream+json; cherry=true; nuts=false'))
|
222
|
+
end
|
223
|
+
|
224
|
+
context 'does not add json for an already json identifier' do
|
225
|
+
it 'non-parameterized mediatypes simply ignore adding the suffix' do
|
226
|
+
plain_application_json = described_class.new('application/json')
|
227
|
+
|
228
|
+
expect(plain_application_json + 'json').to \
|
229
|
+
eq(plain_application_json)
|
230
|
+
end
|
231
|
+
it 'parameterized mediatypes still keeps them' do
|
232
|
+
parameterized_application_json = described_class.new('application/json; cherry=true; nuts=false')
|
233
|
+
expect(parameterized_application_json + 'json').to \
|
234
|
+
eq(parameterized_application_json)
|
235
|
+
end
|
222
236
|
end
|
223
237
|
end
|
224
238
|
end
|
data/spec/praxis/request_spec.rb
CHANGED
@@ -17,9 +17,9 @@ describe Praxis::Request do
|
|
17
17
|
|
18
18
|
let(:context) do
|
19
19
|
{
|
20
|
-
params: [Attributor::
|
21
|
-
headers: [Attributor::
|
22
|
-
payload: [Attributor::
|
20
|
+
params: [Attributor::ROOT_PREFIX, "params".freeze],
|
21
|
+
headers: [Attributor::ROOT_PREFIX, "headers".freeze],
|
22
|
+
payload: [Attributor::ROOT_PREFIX, "payload".freeze]
|
23
23
|
}.freeze
|
24
24
|
end
|
25
25
|
|