praxis 2.0.pre.29 → 2.0.pre.30

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -1
  3. data/.ruby-version +1 -1
  4. data/CHANGELOG.md +24 -0
  5. data/SELECTOR_NOTES.txt +0 -0
  6. data/lib/praxis/application.rb +4 -0
  7. data/lib/praxis/blueprint.rb +13 -1
  8. data/lib/praxis/blueprint_attribute_group.rb +29 -0
  9. data/lib/praxis/docs/open_api/schema_object.rb +8 -7
  10. data/lib/praxis/endpoint_definition.rb +1 -1
  11. data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +11 -11
  12. data/lib/praxis/extensions/attribute_filtering/filter_tree_node.rb +0 -1
  13. data/lib/praxis/extensions/attribute_filtering/filters_parser.rb +1 -1
  14. data/lib/praxis/extensions/pagination/active_record_pagination_handler.rb +54 -4
  15. data/lib/praxis/extensions/pagination/ordering_params.rb +38 -10
  16. data/lib/praxis/extensions/pagination/pagination_handler.rb +3 -3
  17. data/lib/praxis/extensions/pagination/sequel_pagination_handler.rb +1 -1
  18. data/lib/praxis/mapper/resource.rb +155 -14
  19. data/lib/praxis/mapper/selector_generator.rb +248 -46
  20. data/lib/praxis/media_type_identifier.rb +1 -1
  21. data/lib/praxis/multipart/part.rb +2 -2
  22. data/lib/praxis/plugins/mapper_plugin.rb +4 -3
  23. data/lib/praxis/renderer.rb +1 -1
  24. data/lib/praxis/routing_config.rb +1 -1
  25. data/lib/praxis/tasks/console.rb +21 -26
  26. data/lib/praxis/types/multipart_array.rb +1 -1
  27. data/lib/praxis/version.rb +1 -1
  28. data/lib/praxis.rb +1 -0
  29. data/praxis.gemspec +1 -1
  30. data/spec/functional_library_spec.rb +187 -0
  31. data/spec/praxis/extensions/attribute_filtering/active_record_filter_query_builder_spec.rb +11 -1
  32. data/spec/praxis/extensions/attribute_filtering/filter_tree_node_spec.rb +16 -4
  33. data/spec/praxis/extensions/field_selection/active_record_query_selector_spec.rb +0 -2
  34. data/spec/praxis/extensions/field_selection/sequel_query_selector_spec.rb +0 -2
  35. data/spec/praxis/extensions/pagination/active_record_pagination_handler_spec.rb +111 -25
  36. data/spec/praxis/extensions/pagination/ordering_params_spec.rb +70 -0
  37. data/spec/praxis/mapper/resource_spec.rb +40 -4
  38. data/spec/praxis/mapper/selector_generator_spec.rb +979 -296
  39. data/spec/praxis/request_stages/action_spec.rb +1 -1
  40. data/spec/spec_app/app/controllers/authors.rb +37 -0
  41. data/spec/spec_app/app/controllers/books.rb +31 -0
  42. data/spec/spec_app/app/resources/author.rb +21 -0
  43. data/spec/spec_app/app/resources/base.rb +14 -0
  44. data/spec/spec_app/app/resources/book.rb +43 -0
  45. data/spec/spec_app/app/resources/tag.rb +9 -0
  46. data/spec/spec_app/app/resources/tagging.rb +9 -0
  47. data/spec/spec_app/config/environment.rb +16 -1
  48. data/spec/spec_app/design/media_types/author.rb +13 -0
  49. data/spec/spec_app/design/media_types/book.rb +22 -0
  50. data/spec/spec_app/design/media_types/tag.rb +11 -0
  51. data/spec/spec_app/design/media_types/tagging.rb +10 -0
  52. data/spec/spec_app/design/resources/authors.rb +35 -0
  53. data/spec/spec_app/design/resources/books.rb +39 -0
  54. data/spec/spec_helper.rb +0 -1
  55. data/spec/support/spec_resources.rb +20 -7
  56. data/spec/{praxis/extensions/support → support}/spec_resources_active_model.rb +14 -0
  57. metadata +24 -7
  58. /data/spec/{functional_spec.rb → functional_cloud_spec.rb} +0 -0
  59. /data/spec/{praxis/extensions/support → support}/spec_resources_sequel.rb +0 -0
@@ -255,9 +255,9 @@ module Praxis
255
255
  self.content_type = original_content_type
256
256
  end
257
257
 
258
- def self.dump_for_openapi(example_part)
258
+ def self.dump_for_openapi(_example_part)
259
259
  # TODO: This needs to be structured as OpenAPI requires it
260
- raise "dumping a part for open api not implemented yet"
260
+ raise 'dumping a part for open api not implemented yet'
261
261
  end
262
262
  end
263
263
  end
@@ -54,10 +54,11 @@ module Praxis
54
54
  filters = request.params.filters if request.params.respond_to?(:filters)
55
55
  # Handle filters
56
56
  base_query = domain_model.craft_filter_query(base_query, filters: filters)
57
- # Handle field and nested field selection
58
- base_query = domain_model.craft_field_selection_query(base_query, selectors: selector_generator.selectors)
57
+
58
+ selectors = selector_generator.selectors
59
+ base_query = domain_model.craft_field_selection_query(base_query, selectors: selectors)
59
60
  # handle pagination and ordering if the pagination extention is included
60
- base_query = domain_model.craft_pagination_query(base_query, pagination: _pagination) if respond_to?(:_pagination)
61
+ base_query = domain_model.craft_pagination_query(base_query, pagination: _pagination, selectors: selectors) if respond_to?(:_pagination)
61
62
 
62
63
  base_query
63
64
  end
@@ -12,7 +12,7 @@ module Praxis
12
12
  @context = context
13
13
 
14
14
  first = Attributor.humanize_context(context[0..10])
15
- last = Attributor.humanize_context(context[-5..-1])
15
+ last = Attributor.humanize_context(context[-5..])
16
16
  pretty_context = "#{first}...#{last}"
17
17
  super("SystemStackError in rendering #{object.class} with context: #{pretty_context}")
18
18
  end
@@ -25,7 +25,7 @@ module Praxis
25
25
  when ''
26
26
  @prefix_segments = []
27
27
  when ABSOLUTE_PATH_REGEX
28
- @prefix_segments = Array(prefix[1..-1])
28
+ @prefix_segments = Array(prefix[1..])
29
29
  else
30
30
  @prefix_segments << prefix
31
31
  end
@@ -3,36 +3,31 @@
3
3
  namespace :praxis do
4
4
  desc 'Run interactive pry/irb console'
5
5
  task :console do
6
- have_pry = false
7
-
8
- begin
9
- # Use pry if available; require pry _before_ environment to maximize
10
- # debuggability.
11
- require 'pry'
12
- have_pry = true
13
- rescue LoadError
14
- # Fall back on irb
15
- require 'irb'
16
- end
6
+ # Use irb if available (which it almost always is).
7
+ require 'irb'
17
8
 
18
9
  Rake::Task['praxis:environment'].invoke
19
10
 
20
- if have_pry
21
- Praxis::Application.instance.pry
22
- else
23
- # Keep IRB.setup from complaining about bad ARGV options
24
- old_argv = ARGV.dup
25
- ARGV.clear
26
- IRB.setup nil
27
- ARGV.concat(old_argv)
11
+ # Keep IRB.setup from complaining about bad ARGV options
12
+ old_argv = ARGV.dup
13
+ ARGV.clear
14
+ IRB.setup nil
15
+ ARGV.concat(old_argv)
16
+
17
+ # Allow reentrant IRB
18
+ IRB.conf[:MAIN_CONTEXT] = IRB::Irb.new.context
19
+ require 'irb/ext/multi-irb'
28
20
 
29
- # Allow reentrant IRB
30
- IRB.conf[:MAIN_CONTEXT] = IRB::Irb.new.context
31
- require 'irb/ext/multi-irb'
21
+ # Remove main object from prompt (its stringify is not useful)
22
+ nickname = File.basename(::Praxis::Application.instance.root)
23
+ IRB.conf[:PROMPT][:DEFAULT] = {
24
+ PROMPT_I: "%N(#{nickname}):%03n:%i> ",
25
+ PROMPT_N: "%N(#{nickname}):%03n:%i> ",
26
+ PROMPT_S: "%N(#{nickname}):%03n:%i%l ",
27
+ PROMPT_C: "%N(#{nickname}):%03n:%i* "
28
+ }
32
29
 
33
- # Use some special initialization magic to ensure that 'self' in the
34
- # IRB session refers to Praxis::Application.instance.
35
- IRB.irb(nil, Praxis::Application.instance)
36
- end
30
+ # Set the IRB main object.
31
+ IRB.irb(nil, Praxis::Application.instance)
37
32
  end
38
33
  end
@@ -372,7 +372,7 @@ module Praxis
372
372
  end
373
373
 
374
374
  def self.dump_for_openapi(example)
375
- example.map {|part| MultipartPart.dump_for_openapi(part)}
375
+ example.map { |part| MultipartPart.dump_for_openapi(part) }
376
376
  end
377
377
  end
378
378
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Praxis
4
- VERSION = '2.0.pre.29'
4
+ VERSION = '2.0.pre.30'
5
5
  end
data/lib/praxis.rb CHANGED
@@ -49,6 +49,7 @@ module Praxis
49
49
 
50
50
  # Sort of part of the old Blueprints gem...but they're really not scoped...
51
51
  autoload :Blueprint, 'praxis/blueprint'
52
+ autoload :BlueprintAttributeGroup, 'praxis/blueprint_attribute_group'
52
53
  autoload :FieldExpander, 'praxis/field_expander'
53
54
  autoload :Renderer, 'praxis/renderer'
54
55
 
data/praxis.gemspec CHANGED
@@ -15,7 +15,7 @@ Gem::Specification.new do |spec|
15
15
 
16
16
  spec.homepage = 'https://github.com/praxis/praxis'
17
17
  spec.license = 'MIT'
18
- spec.required_ruby_version = '>=2.5'
18
+ spec.required_ruby_version = '>=2.7'
19
19
 
20
20
  spec.require_paths = ['lib']
21
21
  spec.files = `git ls-files -z`.split("\x0")
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe 'Functional specs for books with connected DB' do
6
+ def app
7
+ Praxis::Application.instance
8
+ end
9
+ let(:parsed_response) { JSON.parse(subject.body, symbolize_names: true) }
10
+ context 'books' do
11
+ context 'index' do
12
+ let(:filters_q) { '' }
13
+ let(:fields_q) { '' }
14
+ let(:order_q) { '' }
15
+ subject do
16
+ get '/api/books', api_version: '1.0', fields: fields_q, filters: filters_q, order: order_q
17
+ end
18
+
19
+ context 'all books' do
20
+ it 'is successful' do
21
+ expect(subject).to be_successful
22
+ expect(subject.headers['Content-Type']).to eq('application/vnd.acme.book; type=collection')
23
+ expect(parsed_response.size).to eq ActiveBook.count
24
+ end
25
+ end
26
+
27
+ context 'with deep fields including a group' do
28
+ let(:fields_q) { 'id,name,author{name,books{name}},tags,grouped{name,moar_tags}' }
29
+ it 'is successful' do
30
+ expect(subject).to be_successful
31
+ first_book = parsed_response.first
32
+ expect(first_book.keys).to match_array(%i[id name author tags grouped])
33
+ expect(first_book[:author].keys).to match_array(%i[name books])
34
+ expect(first_book[:grouped].keys).to match_array(%i[name moar_tags])
35
+ expect(first_book[:grouped][:name]).to eq(first_book[:name])
36
+ # grouped.moar_tags is exactly the same result as tags, but tucked in a group with a different name
37
+ expect(first_book[:grouped][:moar_tags]).to eq(first_book[:tags])
38
+ end
39
+ end
40
+
41
+ context 'with deep filters' do
42
+ let(:filters_q) { 'author.name=author*' }
43
+ it 'is successful' do
44
+ expect(subject).to be_successful
45
+ expect(parsed_response.size).to eq ActiveBook.joins(:author).where('active_authors.name LIKE "author%"').count
46
+ end
47
+ end
48
+ context 'with more deep filters' do
49
+ let(:filters_q) { 'tags.name=green' }
50
+ it 'is successful' do
51
+ expect(subject).to be_successful
52
+ num_books_with_green_tags = ActiveBook.joins(:tags).where('active_tags.name': 'green').count
53
+ expect(num_books_with_green_tags).to be > 0
54
+ expect(parsed_response.size).to eq num_books_with_green_tags
55
+ end
56
+ end
57
+
58
+ context 'with ordering' do
59
+ context 'by direct attribute' do
60
+ let(:order_q) { '-id' }
61
+ it 'is successful' do
62
+ expect(subject).to be_successful
63
+ expect(parsed_response.map { |book| book[:id] }).to eq ActiveBook.distinct.order('id DESC').pluck(:id)
64
+ end
65
+ end
66
+ context 'by nested attribute (but without any fields or filter using the attribute)' do
67
+ let(:order_q) { '-author.name' }
68
+ it 'is successful' do
69
+ expect(subject).to be_successful
70
+ ids = ActiveBook.distinct.left_outer_joins(:author).order('active_authors.name DESC').pluck(:id)
71
+
72
+ expect(parsed_response.map { |book| book[:id] }).to eq ids
73
+ end
74
+ end
75
+ context 'combined with filters' do
76
+ context 'by nested attribute (with fields using the same association, but not the same leaf)' do
77
+ let(:order_q) { '-author.name' }
78
+ let(:fields_q) { 'id,author{id}' }
79
+ it { expect(subject).to be_successful }
80
+ end
81
+
82
+ context 'by nested attribute (with a filter using the same association, but not the same leaf)' do
83
+ let(:order_q) { '-author.name' }
84
+ let(:filters_q) { 'author.id=1' }
85
+ it { expect(subject).to be_successful }
86
+ end
87
+
88
+ context 'by nested attribute (with a filter using the same association AND the same leaf)' do
89
+ let(:order_q) { '-author.name' }
90
+ let(:filters_q) { 'author.name=Author1' }
91
+ it { expect(subject).to be_successful }
92
+ end
93
+
94
+ context 'Using ! on a leaf also makes the order use the alias' do
95
+ let(:order_q) { '-author.name' }
96
+ let(:filters_q) { 'author.name!' }
97
+ it { expect(subject).to be_successful }
98
+ end
99
+
100
+ context 'Using ! on an overlapping association also makes the order use the alias' do
101
+ let(:order_q) { '-author.name' }
102
+ let(:filters_q) { 'id=1&author!&author.name!&tags.name=red' }
103
+ it { expect(subject).to be_successful }
104
+ end
105
+ end
106
+ end
107
+ end
108
+
109
+ context 'show' do
110
+ let(:fields_q) { '' }
111
+ subject do
112
+ get '/api/books/1', api_version: '1.0', fields: fields_q
113
+ end
114
+ it 'is successful' do
115
+ expect(subject).to be_successful
116
+ expect(parsed_response[:name]).to eq ActiveBook.find_by(id: 1).simple_name
117
+ end
118
+
119
+ context 'with grouped attributes' do
120
+ let(:fields_q) { 'id,grouped{id,name}' }
121
+ it 'is successful' do
122
+ expect(subject).to be_successful
123
+ model = ActiveBook.find_by(id: 1)
124
+ expect(parsed_response[:id]).to eq model.id
125
+ expect(parsed_response[:grouped]).to eq({ id: model.id, name: model.simple_name })
126
+ end
127
+
128
+ context 'it does not invoke the ones that are not rendered' do
129
+ let(:fields_q) { 'id,grouped{id}' }
130
+ it 'is successful' do
131
+ expect(subject).to be_successful
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
137
+
138
+ context 'authors (which have a base query that joins itself)' do
139
+ let(:filters_q) { '' }
140
+ let(:fields_q) { '' }
141
+ let(:order_q) { '' }
142
+ subject do
143
+ get '/api/authors', api_version: '1.0', fields: fields_q, filters: filters_q, order: order_q
144
+ end
145
+
146
+ context 'all authors' do
147
+ # Authors have a base query that restricts authors whom have books that start with 'book' and that reach authors with id > 0
148
+ let(:base_query) do
149
+ ActiveAuthor.joins(books: :author).where('active_books.simple_name LIKE ?', 'book%').where('authors_active_books.id > ?', 0)
150
+ end
151
+ it 'is successful' do
152
+ expect(subject).to be_successful
153
+ expect(subject.headers['Content-Type']).to eq('application/vnd.acme.author; type=collection')
154
+
155
+ expect(parsed_response.size).to eq base_query.count
156
+ end
157
+ context 'ordering' do
158
+ context 'using direct attributes' do
159
+ let(:order_q) { '-name,id' }
160
+ it { expect(subject).to be_successful }
161
+ end
162
+ context 'using nested attributes' do
163
+ let(:order_q) { '-name,books.name' }
164
+ it 'is successful' do
165
+ expect(subject).to be_successful
166
+ ids = base_query.order('active_authors.name DESC', 'active_books.simple_name DESC').pluck(:id)
167
+
168
+ expect(parsed_response.map { |book| book[:id] }).to eq ids
169
+ end
170
+ end
171
+ end
172
+ context 'filtering and sorting' do
173
+ context 'using the same tables, including the base query one' do
174
+ let(:order_q) { '-name,books.name' }
175
+ let(:filters_q) { 'books.name!' }
176
+ let(:fields_q) { 'id,books{name}' }
177
+ it 'is successful' do
178
+ expect(subject).to be_successful
179
+ ids = base_query.order('active_authors.name DESC', 'active_books.simple_name DESC').pluck(:id)
180
+
181
+ expect(parsed_response.map { |book| book[:id] }).to eq ids
182
+ end
183
+ end
184
+ end
185
+ end
186
+ end
187
+ end
@@ -2,7 +2,6 @@
2
2
 
3
3
  require 'spec_helper'
4
4
 
5
- require_relative '../support/spec_resources_active_model'
6
5
  require 'praxis/extensions/attribute_filtering'
7
6
  require 'praxis/extensions/attribute_filtering/active_record_filter_query_builder'
8
7
 
@@ -358,6 +357,17 @@ describe Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
358
357
  SQL
359
358
  end
360
359
 
360
+ context 'adds multiple where clauses for same nested relationship join even if it is a ! or !! filter without a value (instead of multiple joins with 1 clause each)' do
361
+ let(:filters_string) { 'taggings!&(taggings.label=primary|taggings.tag_id=2)' }
362
+ it_behaves_like 'subject_equivalent_to', ActiveBook.joins(:taggings).where('active_taggings.label' => 'primary')
363
+ .or(ActiveBook.joins(:taggings).where('active_taggings.tag_id' => 2))
364
+ it_behaves_like 'subject_matches_sql', <<~SQL
365
+ SELECT "active_books".* FROM "active_books"
366
+ LEFT OUTER JOIN "active_taggings" "/taggings" ON "/taggings"."book_id" = "active_books"."id"
367
+ WHERE ("/taggings"."id" IS NOT NULL) AND ("/taggings"."label" = 'primary' OR "/taggings"."tag_id" = 2)
368
+ SQL
369
+ end
370
+
361
371
  context 'works well with ORs at a parent table along with joined associations with no rows' do
362
372
  let(:filters_string) { 'name=Book1005|category!!' }
363
373
  it_behaves_like 'subject_equivalent_to', ActiveBook.where.missing(:category)
@@ -13,19 +13,22 @@ describe Praxis::Extensions::AttributeFiltering::FilterTreeNode do
13
13
  { name: 'rel1.a1', op: '=', value: 1 },
14
14
  { name: 'rel1.a2', op: '=', value: 2 },
15
15
  { name: 'rel1.rel2.b1', op: '=', value: 11 },
16
- { name: 'rel1.rel2.b2', op: '=', value: 12, node_object: dummy_object }
16
+ { name: 'rel1.rel2.b2', op: '=', value: 12, node_object: dummy_object },
17
+ { name: 'rel3', op: '!', value: nil },
18
+ { name: 'rel4.rel5', op: '!', value: nil }
17
19
  ]
18
20
  end
19
21
  context 'initialization' do
20
22
  subject { described_class.new(filters) }
21
23
  it 'holds the top conditions and the child in a TreeNode' do
22
24
  expect(subject.path).to eq([])
23
- expect(subject.conditions.size).to eq(2)
25
+ expect(subject.conditions.size).to eq(3)
24
26
  expect(subject.conditions.map { |i| i.slice(:name, :op, :value) }).to eq([
25
27
  { name: 'one', op: '>', value: 1 },
26
- { name: 'one', op: '<', value: 10 }
28
+ { name: 'one', op: '<', value: 10 },
29
+ { name: 'rel3', op: '!', value: nil }
27
30
  ])
28
- expect(subject.children.keys).to eq(['rel1'])
31
+ expect(subject.children.keys).to eq(%w[rel1 rel4])
29
32
  expect(subject.children['rel1']).to be_kind_of(described_class)
30
33
  end
31
34
 
@@ -54,6 +57,15 @@ describe Praxis::Extensions::AttributeFiltering::FilterTreeNode do
54
57
  { name: 'b2', op: '=', value: 12 }
55
58
  ])
56
59
  expect(rel1rel2.children.keys).to be_empty
60
+
61
+ # ! operators have the condition on the association, not the leaf (i.e,. have a condition, no children)
62
+ rel4 = subject.children['rel4']
63
+ expect(rel4.path).to eq(['rel4'])
64
+ expect(rel4.conditions.size).to eq(1)
65
+ expect(rel4.conditions.map { |i| i.slice(:name, :op, :value) }).to eq([
66
+ { name: 'rel5', op: '!', value: nil }
67
+ ])
68
+ expect(rel4.children.keys).to be_empty
57
69
  end
58
70
  end
59
71
  end
@@ -2,8 +2,6 @@
2
2
 
3
3
  require 'spec_helper'
4
4
 
5
- require_relative '../support/spec_resources_active_model'
6
-
7
5
  describe Praxis::Extensions::FieldSelection::ActiveRecordQuerySelector do
8
6
  let(:selector_fields) do
9
7
  {
@@ -40,8 +40,6 @@ describe Praxis::Extensions::FieldSelection::SequelQuerySelector do
40
40
  # Pay the price for creating and connecting only in this spec instead in spec helper
41
41
  # this way all other specs do not need to be slower and it's a better TDD experience
42
42
 
43
- require_relative '../support/spec_resources_sequel'
44
-
45
43
  let(:selector_fields) do
46
44
  {
47
45
  name: true,
@@ -2,31 +2,26 @@
2
2
 
3
3
  require 'spec_helper'
4
4
 
5
- require_relative '../support/spec_resources_active_model'
6
5
  require 'praxis/extensions/pagination'
7
6
 
8
- class Book < Praxis::MediaType
9
- attributes do
10
- attribute :id, Integer
11
- attribute :simple_name, String
12
- attribute :category_uuid, String
7
+ describe Praxis::Extensions::Pagination::ActiveRecordPaginationHandler do
8
+ let(:book_pagination_params_attribute) do
9
+ Attributor::Attribute.new(Praxis::Types::PaginationParams.for(Book)) do
10
+ max_items 3
11
+ page_size 2
12
+ # disallow :paging
13
+ default by: :id
14
+ end
13
15
  end
14
- end
15
- Book.finalize!
16
- BookPaginationParamsAttribute = Attributor::Attribute.new(Praxis::Types::PaginationParams.for(Book)) do
17
- max_items 3
18
- page_size 2
19
- # disallow :paging
20
- default by: :id
21
- end
22
16
 
23
- BookOrderingParamsAttribute = Attributor::Attribute.new(Praxis::Types::OrderingParams.for(Book)) do
24
- enforce_for :all
25
- end
17
+ let(:book_ordering_params_attribute) do
18
+ Attributor::Attribute.new(Praxis::Types::OrderingParams.for(Book)) do
19
+ enforce_for :all
20
+ end
21
+ end
26
22
 
27
- describe Praxis::Extensions::Pagination::ActiveRecordPaginationHandler do
28
23
  shared_examples 'sorts_the_same' do |op, expected|
29
- let(:order_params) { BookOrderingParamsAttribute.load(op) }
24
+ let(:order_params) { book_ordering_params_attribute.load(op) }
30
25
  it do
31
26
  loaded_ids = subject.all.map(&:id)
32
27
  expected_ids = expected.all.map(&:id)
@@ -35,7 +30,7 @@ describe Praxis::Extensions::Pagination::ActiveRecordPaginationHandler do
35
30
  end
36
31
 
37
32
  shared_examples 'paginates_the_same' do |par, expected|
38
- let(:paginator_params) { BookPaginationParamsAttribute.load(par) }
33
+ let(:paginator_params) { book_pagination_params_attribute.load(par) }
39
34
  it do
40
35
  loaded_ids = subject.all.map(&:id)
41
36
  expected_ids = expected.all.map(&:id)
@@ -43,7 +38,7 @@ describe Praxis::Extensions::Pagination::ActiveRecordPaginationHandler do
43
38
  end
44
39
  end
45
40
 
46
- let(:query) { ActiveBook }
41
+ let(:query) { ActiveBook.includes(:author) }
47
42
  let(:table) { ActiveBook.table_name }
48
43
  let(:paginator_params) { nil }
49
44
  let(:order_params) { nil }
@@ -52,7 +47,7 @@ describe Praxis::Extensions::Pagination::ActiveRecordPaginationHandler do
52
47
  end
53
48
 
54
49
  context '.paginate' do
55
- subject { described_class.paginate(query, pagination) }
50
+ subject { described_class.paginate(query, pagination, root_resource: ActiveBookResource) }
56
51
 
57
52
  context 'empty struct' do
58
53
  let(:paginator_params) { nil }
@@ -93,7 +88,7 @@ describe Praxis::Extensions::Pagination::ActiveRecordPaginationHandler do
93
88
  end
94
89
 
95
90
  context 'including order' do
96
- let(:order_params) { BookOrderingParamsAttribute.load(op_string) }
91
+ let(:order_params) { book_ordering_params_attribute.load(op_string) }
97
92
 
98
93
  context 'when compatible with cursor' do
99
94
  let(:op_string) { 'id' }
@@ -104,7 +99,7 @@ describe Praxis::Extensions::Pagination::ActiveRecordPaginationHandler do
104
99
 
105
100
  context 'when incompatible with cursor' do
106
101
  let(:op_string) { 'id' }
107
- let(:paginator_params) { BookPaginationParamsAttribute.load('by=simple_name,items=3') }
102
+ let(:paginator_params) { book_pagination_params_attribute.load('by=simple_name,items=3') }
108
103
  it do
109
104
  expect { subject.all }.to raise_error(described_class::PaginationException, /is incompatible with pagination/)
110
105
  end
@@ -113,7 +108,7 @@ describe Praxis::Extensions::Pagination::ActiveRecordPaginationHandler do
113
108
  end
114
109
 
115
110
  context '.order' do
116
- subject { described_class.order(query, pagination.order) }
111
+ subject { described_class.order(query, pagination.order, root_resource: ActiveBookResource) }
117
112
 
118
113
  it 'does not change the query with an empty struct' do
119
114
  expect(subject).to be(query)
@@ -127,5 +122,96 @@ describe Praxis::Extensions::Pagination::ActiveRecordPaginationHandler do
127
122
  ::ActiveBook.order(simple_name: :desc, id: :asc)
128
123
  it_behaves_like 'sorts_the_same', '-simple_name,-id',
129
124
  ::ActiveBook.order(simple_name: :desc, id: :desc)
125
+
126
+ context 'inner joining authors' do
127
+ let(:query) { ActiveBook.joins(:author) }
128
+ it_behaves_like 'sorts_the_same', '-author.name',
129
+ ::ActiveBook.joins(:author).references(:author).order('active_authors.name': :desc)
130
+ end
131
+
132
+ context 'with mapped order fields' do
133
+ it_behaves_like 'sorts_the_same', 'name', # name => simple_name
134
+ ::ActiveBook.order(simple_name: :asc)
135
+
136
+ context 'with deeper joins that map names' do
137
+ let(:query) { ActiveBook.joins(:author) }
138
+ context 'of intermediate associations (writer => author)' do
139
+ it_behaves_like 'sorts_the_same', '-writer.name',
140
+ ::ActiveBook.joins(:author).references(:author).order('active_authors.name': :desc)
141
+ end
142
+ context 'of leaf properties (display_name => name)' do
143
+ it_behaves_like 'sorts_the_same', '-author.display_name',
144
+ ::ActiveBook.joins(:author).references(:author).order('active_authors.name': :desc)
145
+ end
146
+ context 'of both intermediate and leaf properties ((writer => author AND display_name => name)' do
147
+ it_behaves_like 'sorts_the_same', '-writer.display_name,author.id,',
148
+ ::ActiveBook.joins(:author).references(:author).order('active_authors.name': :desc, 'active_authors.id': :asc)
149
+ end
150
+ context 'of deep associations' do
151
+ it_behaves_like 'sorts_the_same', '-writer.books.name,author.id',
152
+ ::ActiveBook.joins(author: :books).references(:author, 'books_active_authors')
153
+ .order('books_active_authors.simple_name': :desc, 'active_authors.id': :asc)
154
+ end
155
+ end
156
+ end
157
+ end
158
+
159
+ context '.association_info_for' do
160
+ let(:resource) { Resources::Book }
161
+ subject { described_class.association_info_for(resource, path.split('.')) }
162
+ context 'book -> author.name' do
163
+ let(:path) { 'author.name' }
164
+ it 'works' do
165
+ expect(subject).to eq({
166
+ resource: Resources::Author,
167
+ includes: { 'author' => {} },
168
+ attribute: 'name'
169
+ })
170
+ end
171
+ end
172
+
173
+ context 'book -> taggings.tag.name' do
174
+ let(:path) { 'taggings.tag.name' }
175
+ it 'works' do
176
+ expect(subject).to eq({
177
+ resource: Resources::Tag,
178
+ includes: { 'taggings' => { 'tag' => {} } },
179
+ attribute: 'name'
180
+ })
181
+ end
182
+ end
183
+ context 'book -> tags' do
184
+ let(:path) { 'tags' }
185
+ it 'works' do
186
+ expect(subject).to eq({
187
+ resource: Resources::Tag,
188
+ includes: { 'tags' => {} },
189
+ attribute: nil
190
+ })
191
+ end
192
+ end
193
+
194
+ context 'with mapped/aliased names' do
195
+ context 'book -> writer.name' do
196
+ let(:path) { 'writer.name' }
197
+ it 'works' do
198
+ expect(subject).to eq({
199
+ resource: Resources::Author,
200
+ includes: { 'author' => {} },
201
+ attribute: 'name'
202
+ })
203
+ end
204
+ end
205
+ context 'book -> writer.name' do
206
+ let(:path) { 'writer.display_name' }
207
+ it 'works' do
208
+ expect(subject).to eq({
209
+ resource: Resources::Author,
210
+ includes: { 'author' => {} },
211
+ attribute: 'name'
212
+ })
213
+ end
214
+ end
215
+ end
130
216
  end
131
217
  end