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

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