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.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -1
- data/.ruby-version +1 -1
- data/CHANGELOG.md +24 -0
- data/SELECTOR_NOTES.txt +0 -0
- data/lib/praxis/application.rb +4 -0
- data/lib/praxis/blueprint.rb +13 -1
- data/lib/praxis/blueprint_attribute_group.rb +29 -0
- data/lib/praxis/docs/open_api/schema_object.rb +8 -7
- data/lib/praxis/endpoint_definition.rb +1 -1
- data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +11 -11
- data/lib/praxis/extensions/attribute_filtering/filter_tree_node.rb +0 -1
- data/lib/praxis/extensions/attribute_filtering/filters_parser.rb +1 -1
- data/lib/praxis/extensions/pagination/active_record_pagination_handler.rb +54 -4
- data/lib/praxis/extensions/pagination/ordering_params.rb +38 -10
- data/lib/praxis/extensions/pagination/pagination_handler.rb +3 -3
- data/lib/praxis/extensions/pagination/sequel_pagination_handler.rb +1 -1
- data/lib/praxis/mapper/resource.rb +155 -14
- data/lib/praxis/mapper/selector_generator.rb +248 -46
- data/lib/praxis/media_type_identifier.rb +1 -1
- data/lib/praxis/multipart/part.rb +2 -2
- data/lib/praxis/plugins/mapper_plugin.rb +4 -3
- data/lib/praxis/renderer.rb +1 -1
- data/lib/praxis/routing_config.rb +1 -1
- data/lib/praxis/tasks/console.rb +21 -26
- data/lib/praxis/types/multipart_array.rb +1 -1
- data/lib/praxis/version.rb +1 -1
- data/lib/praxis.rb +1 -0
- data/praxis.gemspec +1 -1
- data/spec/functional_library_spec.rb +187 -0
- data/spec/praxis/extensions/attribute_filtering/active_record_filter_query_builder_spec.rb +11 -1
- data/spec/praxis/extensions/attribute_filtering/filter_tree_node_spec.rb +16 -4
- data/spec/praxis/extensions/field_selection/active_record_query_selector_spec.rb +0 -2
- data/spec/praxis/extensions/field_selection/sequel_query_selector_spec.rb +0 -2
- data/spec/praxis/extensions/pagination/active_record_pagination_handler_spec.rb +111 -25
- data/spec/praxis/extensions/pagination/ordering_params_spec.rb +70 -0
- data/spec/praxis/mapper/resource_spec.rb +40 -4
- data/spec/praxis/mapper/selector_generator_spec.rb +979 -296
- data/spec/praxis/request_stages/action_spec.rb +1 -1
- data/spec/spec_app/app/controllers/authors.rb +37 -0
- data/spec/spec_app/app/controllers/books.rb +31 -0
- data/spec/spec_app/app/resources/author.rb +21 -0
- data/spec/spec_app/app/resources/base.rb +14 -0
- data/spec/spec_app/app/resources/book.rb +43 -0
- data/spec/spec_app/app/resources/tag.rb +9 -0
- data/spec/spec_app/app/resources/tagging.rb +9 -0
- data/spec/spec_app/config/environment.rb +16 -1
- data/spec/spec_app/design/media_types/author.rb +13 -0
- data/spec/spec_app/design/media_types/book.rb +22 -0
- data/spec/spec_app/design/media_types/tag.rb +11 -0
- data/spec/spec_app/design/media_types/tagging.rb +10 -0
- data/spec/spec_app/design/resources/authors.rb +35 -0
- data/spec/spec_app/design/resources/books.rb +39 -0
- data/spec/spec_helper.rb +0 -1
- data/spec/support/spec_resources.rb +20 -7
- data/spec/{praxis/extensions/support → support}/spec_resources_active_model.rb +14 -0
- metadata +24 -7
- /data/spec/{functional_spec.rb → functional_cloud_spec.rb} +0 -0
- /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(
|
258
|
+
def self.dump_for_openapi(_example_part)
|
259
259
|
# TODO: This needs to be structured as OpenAPI requires it
|
260
|
-
raise
|
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
|
-
|
58
|
-
|
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
|
data/lib/praxis/renderer.rb
CHANGED
@@ -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
|
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
|
data/lib/praxis/tasks/console.rb
CHANGED
@@ -3,36 +3,31 @@
|
|
3
3
|
namespace :praxis do
|
4
4
|
desc 'Run interactive pry/irb console'
|
5
5
|
task :console do
|
6
|
-
|
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
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
-
|
30
|
-
|
31
|
-
|
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
|
-
|
34
|
-
|
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
|
data/lib/praxis/version.rb
CHANGED
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.
|
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(
|
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([
|
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
|
@@ -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
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
24
|
-
|
25
|
-
|
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) {
|
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) {
|
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) {
|
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) {
|
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
|