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.
- 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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a2ffb701843f77a4f33c4b2628cdc7b609c5bb8b1d50bf0df454a77eded14ca3
|
4
|
+
data.tar.gz: 34852fb86cd240a18701aa013a1d65508c17d9c8831ecf5176efa9c73f7b5339
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6578cd3f42a3f5dde6f32f9a860169e90f68d20c5a58dddefee2511e51e18368ec81a8ee5c75e60581ee53134e44940cbdad079a9e4c460e1b65121bea613b5b
|
7
|
+
data.tar.gz: 59073268280c18c2a9e77e626c5c75503c45d5b64351bd67898861d68b1f952b1f9c8c124af813049fc4b6aaed87cc74860d6561744c654343f00a60a1c59e4f
|
data/.rubocop.yml
CHANGED
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
|
1
|
+
3.1.1
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,30 @@
|
|
2
2
|
|
3
3
|
## next
|
4
4
|
|
5
|
+
## 2.0.pre.30
|
6
|
+
* A few cleanup and robustness additions:
|
7
|
+
* OpenAPI: Disable overriding a description when the schema is a ref (there are known issues with UI browsers)
|
8
|
+
* Internal: use `_pk` in batch processor invocation instead of `id` (resources will now have a `_pk` method which defaults to `id`)
|
9
|
+
* Bumped gemspec Ruby dependency to >=2.7 (but note, that this is just a little relaxed for older codebase, we're fully building for 3.x)
|
10
|
+
* Backwards incompatible changes:
|
11
|
+
* Enforces property names are symbols (before strings were allowed)
|
12
|
+
* Resource properties, using the `as:` option, are now enforced to be real association names (will not accept other resource names and unroll their dependencies)
|
13
|
+
* Deprecated the `:through` option for a property. You can just use `as:` with a long, dot-separated association path directly.
|
14
|
+
* Enhanced ordering semantics for pagination to allow for sorting of deep associated fields:
|
15
|
+
* Right now, you can sort by fields such as `books.author.name` as one of the sorting components (with `+` or `-` still available)
|
16
|
+
* Introduced better attribute grouping concepts, that help in defining subgroups of attributes of the same object, and allow lazy loading of only partial subsets so that one can have expensive computations on some of them, but they will never be invoked unless necessary. See MediaType.`group` and Resoruce.`property_group` explanations below.
|
17
|
+
* Introduced a 'group' stanza in MediaTypes, to specify a structure of attributes that exist in the main object, but that we want to neatly expose as a subset (instead of having them unrolled at the top):
|
18
|
+
* You can now use things like `group subinfo do ... end` blocks, defining which attributes to group
|
19
|
+
* Internal: Underneath, the system will create a BlueprintAttributeGroup (instead of a Struct) as a way to ensure that only the individual attributes that need to be rendered, are accessed (and not load the whole struct at once). While the behavior, to the outside, is gonna be identical to a Struct (i.e., exposes attributes as methods), this distinct object implementation is very important as it allows you to have attributes in the subgroup that are expensive to compute, and can be rest assured that they will not be accessed/computed unless they are required for rendering.
|
20
|
+
* Introduced the `property_group` stanza in resources, to indicate that a property contains a substructure of attributes, each of which must be able to be loaded only when necessary. This commonly goes hand in hand with a `group` stanza in the resource's MediaType:
|
21
|
+
* Usage of property group requires the name of the substructure (a symbol), and the associated mediatype that contains the definition of the `group` struct, under the same name of the property.
|
22
|
+
* Internally, this stanza, will define a normal property, and include as dependencies all of the sub attributes read from the MediaType's property, but appending the name (and `_`) to them to avoid collisions.
|
23
|
+
* Also, it will define a method with the property name which will return a Forwarding object, which will delegate each of the attribute methods back to the original self objects. This allows the object to avoid being 'loaded' as a whole as it happens with Struct, therefore only materializing/calling the attribute that we actually need to use, selectively.
|
24
|
+
* For example, if we have the `Book` MediaType which has a group atrribute called `subinfo` with a few attributes (like `name` and `pages`), we can use `property_group :subinfo, Book` on its domain object, so that the system will:
|
25
|
+
* define a `subinfo` property which will depend on `subinfo_name` and `subinfo_pages`
|
26
|
+
* define a `subinfo` method that will return a Forwarding object, that will forward `name` and `pages` methods to `subinfo_name` and `subinfo_pages` methods of the self resource.
|
27
|
+
* with that, we just need to define our `subinfo_name` and `subinfo_page` methods in the resource (and also define property dependencies for them if we need to)
|
28
|
+
|
5
29
|
## 2.0.pre.29
|
6
30
|
* Assorted set of fixes to generate cleaner and more compliant OpenApi documents.
|
7
31
|
* Mostly in the area of multipart generation, and requirements and nullability for OpenApi 3.0
|
data/SELECTOR_NOTES.txt
ADDED
File without changes
|
data/lib/praxis/application.rb
CHANGED
data/lib/praxis/blueprint.rb
CHANGED
@@ -2,6 +2,18 @@
|
|
2
2
|
|
3
3
|
module Praxis
|
4
4
|
class Blueprint
|
5
|
+
class DSLCompiler < Attributor::HashDSLCompiler
|
6
|
+
# group DSL is meant to group a subset of attributes in the media type (instead of presenting all things flat)
|
7
|
+
# It will create a normal attribute, but as a BlueprintAttributeGroup, rather than a Struct, so that we can more easily
|
8
|
+
# pass in objects that respond to the right methods, and avoid a Struct loading them all in hash keys.
|
9
|
+
# For example, if there are computationally intensive attributes in the subset, we want to make sure those functions
|
10
|
+
# aren't invoked by just merely loading, and only really invoked when we've asked to render them
|
11
|
+
# It takes the name of the group, and passes the attributes block that needs to be a subset of the MediaType where the group resides
|
12
|
+
def group(name, **options, &block)
|
13
|
+
attribute(name, Praxis::BlueprintAttributeGroup.for(target.options[:reference]), **options, &block)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
5
17
|
# Simple helper class that can parse the `attribute :foobar` dsl into
|
6
18
|
# an equivalent structure hash. Example:
|
7
19
|
# do
|
@@ -91,7 +103,7 @@ module Praxis
|
|
91
103
|
|
92
104
|
opts[:reference] = (reference || self)
|
93
105
|
|
94
|
-
@options.merge!(opts)
|
106
|
+
@options.merge!(opts.merge(dsl_compiler: DSLCompiler))
|
95
107
|
@block = block
|
96
108
|
|
97
109
|
return @attribute
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Praxis
|
4
|
+
class BlueprintAttributeGroup < Blueprint
|
5
|
+
def self.constructable?
|
6
|
+
true
|
7
|
+
end
|
8
|
+
|
9
|
+
# Construct a new subclass, using attribute_definition to define attributes.
|
10
|
+
def self.construct(attribute_definition, options = {})
|
11
|
+
return self if attribute_definition.nil?
|
12
|
+
|
13
|
+
reference_type = @media_type
|
14
|
+
# Construct a group-derived class with the given mediatype as the reference
|
15
|
+
::Class.new(self) do
|
16
|
+
@reference = reference_type
|
17
|
+
attributes(**options, &attribute_definition)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.for(media_type)
|
22
|
+
return media_type::AttributeGrouping if defined?(media_type::AttributeGrouping) # Cache the grouping class
|
23
|
+
|
24
|
+
::Class.new(self) do
|
25
|
+
@media_type = media_type
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -31,11 +31,13 @@ module Praxis
|
|
31
31
|
|
32
32
|
def dump_schema(shallow: false, allow_ref: false)
|
33
33
|
# We will dump schemas for mediatypes by simply creating a reference to the components' section
|
34
|
-
if type < Attributor::Container && !
|
34
|
+
if type < Attributor::Container && !(type < Praxis::Types::MultipartArray)
|
35
35
|
if (type < Praxis::Blueprint || type < Attributor::Model) && allow_ref && !type.anonymous?
|
36
|
-
# TODO:
|
37
|
-
|
38
|
-
|
36
|
+
# TODO: Technically OpenAPI/JSON schema support passing a description when pointing to a $ref (to override it)
|
37
|
+
# However, it seems that UI browsers like redoc or elements have bugs where if that's done, they get into a loop and crash
|
38
|
+
# so for now, we're gonna avoid overriding the description until that is solved
|
39
|
+
# h = @attribute_options[:description] ? { 'description' => @attribute_options[:description] } : {}
|
40
|
+
h = {}
|
39
41
|
Praxis::Docs::OpenApiGenerator.instance.register_seen_component(type)
|
40
42
|
h.merge!('$ref' => "#/components/schemas/#{type.id}")
|
41
43
|
elsif @collection
|
@@ -44,12 +46,11 @@ module Praxis
|
|
44
46
|
h.merge!(type: 'array', items: items)
|
45
47
|
else # Attributor::Struct, etc
|
46
48
|
required_attributes = (type.describe[:requirements] || []).filter { |r| r[:type] == :all }.map { |r| r[:attributes] }.flatten.compact.uniq
|
47
|
-
props = type.attributes.transform_values
|
49
|
+
props = type.attributes.transform_values do |definition|
|
48
50
|
# if type has an attribute in its requirements all, then it should be marked as required here
|
49
|
-
field_name = type.attributes.keys[index]
|
50
51
|
OpenApi::SchemaObject.new(info: definition).dump_schema(allow_ref: true, shallow: shallow)
|
51
52
|
end
|
52
|
-
h = { type: :object}
|
53
|
+
h = { type: :object }
|
53
54
|
h[:properties] = props if props.presence
|
54
55
|
h[:required] = required_attributes unless required_attributes.empty?
|
55
56
|
end
|
@@ -232,7 +232,7 @@ module Praxis
|
|
232
232
|
action.sister_post_action # Avoid appending prefix
|
233
233
|
else
|
234
234
|
# Make sure to cleanup the leading '/' if any, as we're always adding it below
|
235
|
-
cleaned_path = action.sister_post_action.start_with?('/') ? action.sister_post_action[1
|
235
|
+
cleaned_path = action.sister_post_action.start_with?('/') ? action.sister_post_action[1..] : action.sister_post_action
|
236
236
|
"#{action.route.prefixed_path}/#{cleaned_path}"
|
237
237
|
end
|
238
238
|
|
@@ -74,7 +74,7 @@ module Praxis
|
|
74
74
|
# Mark where clause referencing the appropriate alias IF it's not the root table, as there is no association to reference
|
75
75
|
# If we added root table as a reference, we better make sure it is not quoted, as it actually makes AR to see it as an
|
76
76
|
# unmatched reference and eager loads the whole association (it means eager load ALL the things). Not good.
|
77
|
-
associated_query = associated_query.references(build_reference_value(column_prefix, query: associated_query)) unless for_model.table_name == column_prefix
|
77
|
+
associated_query = associated_query.references(self.class.build_reference_value(column_prefix, query: associated_query)) unless for_model.table_name == column_prefix
|
78
78
|
self.class.add_clause(
|
79
79
|
query: associated_query,
|
80
80
|
column_prefix: column_prefix,
|
@@ -158,7 +158,7 @@ module Praxis
|
|
158
158
|
if association_op
|
159
159
|
neg = association_op == :not_null
|
160
160
|
qr = quote_right_part(query: query, value: nil, column_object: association_key_column, negative: neg)
|
161
|
-
return query.where("#{quote_column_path(query: query, prefix: column_prefix,
|
161
|
+
return query.where("#{quote_column_path(query: query, prefix: column_prefix, column_name: association_key_column.name)} #{qr}")
|
162
162
|
end
|
163
163
|
|
164
164
|
# Add an AND along with the condition, which ensures the left outter join 'exists' for it
|
@@ -168,7 +168,7 @@ module Praxis
|
|
168
168
|
# NOTE: we don't need to do it for conditions applying to the root of the tree (there isn't a join to it)
|
169
169
|
if association_key_column
|
170
170
|
qr = quote_right_part(query: query, value: nil, column_object: association_key_column, negative: true)
|
171
|
-
query = query.where("#{quote_column_path(query: query, prefix: column_prefix,
|
171
|
+
query = query.where("#{quote_column_path(query: query, prefix: column_prefix, column_name: association_key_column.name)} #{qr}")
|
172
172
|
end
|
173
173
|
|
174
174
|
case op
|
@@ -177,14 +177,14 @@ module Praxis
|
|
177
177
|
add_safe_where(query: query, tab: column_prefix, col: column_object, op: 'LIKE', value: likeval)
|
178
178
|
else
|
179
179
|
quoted_right = quote_right_part(query: query, value: value, column_object: column_object, negative: false)
|
180
|
-
query.where("#{quote_column_path(query: query, prefix: column_prefix,
|
180
|
+
query.where("#{quote_column_path(query: query, prefix: column_prefix, column_name: column_object.name)} #{quoted_right}")
|
181
181
|
end
|
182
182
|
when '!='
|
183
183
|
if likeval
|
184
184
|
add_safe_where(query: query, tab: column_prefix, col: column_object, op: 'NOT LIKE', value: likeval)
|
185
185
|
else
|
186
186
|
quoted_right = quote_right_part(query: query, value: value, column_object: column_object, negative: true)
|
187
|
-
query.where("#{quote_column_path(query: query, prefix: column_prefix,
|
187
|
+
query.where("#{quote_column_path(query: query, prefix: column_prefix, column_name: column_object.name)} #{quoted_right}")
|
188
188
|
end
|
189
189
|
when '>'
|
190
190
|
add_safe_where(query: query, tab: column_prefix, col: column_object, op: '>', value: value)
|
@@ -203,13 +203,13 @@ module Praxis
|
|
203
203
|
# rubocop:disable Naming/MethodParameterName
|
204
204
|
def self.add_safe_where(query:, tab:, col:, op:, value:)
|
205
205
|
quoted_value = query.connection.quote_default_expression(value, col)
|
206
|
-
query.where("#{quote_column_path(query: query, prefix: tab,
|
206
|
+
query.where("#{quote_column_path(query: query, prefix: tab, column_name: col.name)} #{op} #{quoted_value}")
|
207
207
|
end
|
208
208
|
# rubocop:enable Naming/MethodParameterName
|
209
209
|
|
210
|
-
def self.quote_column_path(query:, prefix:,
|
210
|
+
def self.quote_column_path(query:, prefix:, column_name:)
|
211
211
|
c = query.connection
|
212
|
-
quoted_column = c.quote_column_name(
|
212
|
+
quoted_column = c.quote_column_name(column_name)
|
213
213
|
if prefix
|
214
214
|
quoted_table = c.quote_table_name(prefix)
|
215
215
|
"#{quoted_table}.#{quoted_column}"
|
@@ -337,7 +337,7 @@ module Praxis
|
|
337
337
|
if ref && ['!', '!!'].include?(condition[:op])
|
338
338
|
cp = (nodetree.path + [condition[:name].to_s]).join(REFERENCES_STRING_SEPARATOR)
|
339
339
|
conditions += [condition.merge(column_prefix: cp, model: model, parent_reflection: ref)]
|
340
|
-
h[condition[:name]] = {}
|
340
|
+
h[condition[:name].to_s] = {}
|
341
341
|
else
|
342
342
|
# Save the parent reflection where the condition applies as well (used later to get assoc keys)
|
343
343
|
conditions += [condition.merge(column_prefix: column_prefix, model: model, parent_reflection: parent_reflection)]
|
@@ -350,14 +350,14 @@ module Praxis
|
|
350
350
|
maj, min, = ActiveRecord.gem_version.segments
|
351
351
|
if maj == 5 || (maj == 6 && min.zero?)
|
352
352
|
# In AR 6 (and 6.0) the references are simple strings
|
353
|
-
def build_reference_value(column_prefix, **_args)
|
353
|
+
def self.build_reference_value(column_prefix, **_args)
|
354
354
|
column_prefix
|
355
355
|
end
|
356
356
|
else
|
357
357
|
# The latest AR versions discard passing references to joins when they're not SqlLiterals ... so let's wrap it
|
358
358
|
# with our class, so that it is a literal (already quoted), but that can still provide the expected "symbol" without quotes
|
359
359
|
# so that our aliasing code can match it.
|
360
|
-
def build_reference_value(column_prefix, query:)
|
360
|
+
def self.build_reference_value(column_prefix, query:)
|
361
361
|
QuasiSqlLiteral.new(quoted: query.connection.quote_table_name(column_prefix), symbolized: column_prefix.to_sym)
|
362
362
|
end
|
363
363
|
end
|
@@ -7,7 +7,6 @@ module Praxis
|
|
7
7
|
attr_reader :path, :conditions, :children
|
8
8
|
|
9
9
|
# Parsed_filters is an Array of {name: X, op: Y, value: Z} ... exactly the format of the FilteringParams.load method
|
10
|
-
# It can also contain a :node_object
|
11
10
|
def initialize(parsed_filters, path: [])
|
12
11
|
@path = path # Array that marks the tree 'path' to this node (with respect to the absolute root)
|
13
12
|
@conditions = [] # Conditions to apply directly to this node
|
@@ -15,14 +15,36 @@ module Praxis
|
|
15
15
|
query.where(query.table[attr].gt(value))
|
16
16
|
end
|
17
17
|
|
18
|
-
def self.order(query, order)
|
18
|
+
def self.order(query, order, root_resource:)
|
19
19
|
return query unless order
|
20
20
|
|
21
21
|
query = query.reorder('')
|
22
|
-
|
23
22
|
order.each do |spec_hash|
|
24
|
-
direction,
|
25
|
-
|
23
|
+
direction, string = spec_hash.first
|
24
|
+
info = association_info_for(root_resource, string.to_s.split('.'))
|
25
|
+
|
26
|
+
# Convert the includes path (it is not a tree), to a column prefix
|
27
|
+
pointer = info[:includes]
|
28
|
+
|
29
|
+
dotted = []
|
30
|
+
loop do
|
31
|
+
break if pointer.empty?
|
32
|
+
|
33
|
+
key, pointer = pointer.first
|
34
|
+
dotted.push(key)
|
35
|
+
end
|
36
|
+
column_prefix = dotted.empty? ? root_resource.model.table_name : ([''] + dotted).join(AttributeFiltering::ActiveRecordFilterQueryBuilder::REFERENCES_STRING_SEPARATOR)
|
37
|
+
|
38
|
+
# If the sorting refers to a deeper association, make sure to add the join and the special reference
|
39
|
+
if column_prefix
|
40
|
+
refval = AttributeFiltering::ActiveRecordFilterQueryBuilder.build_reference_value(column_prefix, query: query)
|
41
|
+
# Outter join hash needs to be a string based hash format!! (if it's in symbols, it won't match it and we'll cause extra joins)
|
42
|
+
query = query.left_outer_joins(info[:includes]).references(refval)
|
43
|
+
end
|
44
|
+
|
45
|
+
quoted_prefix = AttributeFiltering::ActiveRecordFilterQueryBuilder.quote_column_path(query: query, prefix: column_prefix, column_name: info[:attribute])
|
46
|
+
order_clause = Arel.sql(ActiveRecord::Base.sanitize_sql_array("#{quoted_prefix} #{direction}"))
|
47
|
+
query = query.order(order_clause)
|
26
48
|
end
|
27
49
|
query
|
28
50
|
end
|
@@ -38,6 +60,34 @@ module Praxis
|
|
38
60
|
def self.limit(query, limit)
|
39
61
|
query.limit(limit)
|
40
62
|
end
|
63
|
+
|
64
|
+
# Based off of a root resource and an incoming path of dot-separated attributes...
|
65
|
+
# find the leaf attribute and its associated resource (including mapping names of associations/attributes along the way)
|
66
|
+
# as defined by the `order_mapping` stanzas of resources:
|
67
|
+
# resource: final resource the attribute is associated with
|
68
|
+
# includes: a hash in the shape of AR includes, where keys are strings (that is very important)
|
69
|
+
# column_name: final attribute name where this path leads to. Nil if the path ends at an association
|
70
|
+
def self.association_info_for(resource, path)
|
71
|
+
main, *rest = path
|
72
|
+
mapped_name = resource.order_mapping[main.to_sym] || main
|
73
|
+
|
74
|
+
if (association = resource.model.reflections[mapped_name])
|
75
|
+
related_resource = resource.model_map[association.klass]
|
76
|
+
if rest.presence
|
77
|
+
result = association_info_for(related_resource, rest)
|
78
|
+
{ resource: result[:resource], includes: { mapped_name => result[:includes] }, attribute: result[:attribute] }
|
79
|
+
else # Ends with an association (i.e., for ! or !! attributes)
|
80
|
+
{ resource: related_resource, includes: { mapped_name => {} }, attribute: nil }
|
81
|
+
end
|
82
|
+
elsif !rest.presence
|
83
|
+
# Since it is not an association, must be a column name
|
84
|
+
{ resource: resource, includes: {}, attribute: mapped_name }
|
85
|
+
else
|
86
|
+
# Could not find an association and there are more components to cover...something's not right
|
87
|
+
raise 'Error trying to map ordering components to the order_mapping of the resources. ' \
|
88
|
+
"Could not find a mapping for property: #{mapped_name} in resource #{resource.name}. Did you forget to add a mapping for it in the `order_mapping` stanza ?"
|
89
|
+
end
|
90
|
+
end
|
41
91
|
end
|
42
92
|
end
|
43
93
|
end
|
@@ -25,12 +25,15 @@ module Praxis
|
|
25
25
|
class DSLCompiler < Attributor::DSLCompiler
|
26
26
|
def by_fields(*fields)
|
27
27
|
requested = fields.map(&:to_sym)
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
28
|
+
|
29
|
+
errors = []
|
30
|
+
requested.each do |field|
|
31
|
+
if (failed_field = self.class.validate_field(target.media_type, field.to_s.split('.').map(&:to_sym)))
|
32
|
+
errors += ["Cannot order by field: '#{field}'. It seems that the '#{failed_field}' attribute is not defined in the current #{target.media_type} structure (or its subtree)."]
|
33
|
+
end
|
33
34
|
end
|
35
|
+
raise errors.join('\n') unless errors.empty?
|
36
|
+
|
34
37
|
target.fields_allowed = requested
|
35
38
|
end
|
36
39
|
|
@@ -44,6 +47,17 @@ module Praxis
|
|
44
47
|
raise "Error: unknown parameter for the 'enforce_for' : #{which}. Only :all or :first are allowed"
|
45
48
|
end
|
46
49
|
end
|
50
|
+
|
51
|
+
def self.validate_field(type, path)
|
52
|
+
main, rest = path
|
53
|
+
next_attribute = type.respond_to?(:member_attribute) ? type.member_type.attributes[main] : type.attributes[main]
|
54
|
+
|
55
|
+
return main unless next_attribute
|
56
|
+
|
57
|
+
return nil if rest.nil?
|
58
|
+
|
59
|
+
validate_field(next_attribute.type, rest)
|
60
|
+
end
|
47
61
|
end
|
48
62
|
|
49
63
|
# Configurable DEFAULTS
|
@@ -154,9 +168,9 @@ module Praxis
|
|
154
168
|
parsed_order = order.split(',').each_with_object([]) do |order_string, arr|
|
155
169
|
item = case order_string[0]
|
156
170
|
when '-'
|
157
|
-
{ desc: order_string[1
|
171
|
+
{ desc: order_string[1..].to_s }
|
158
172
|
when '+'
|
159
|
-
{ asc: order_string[1
|
173
|
+
{ asc: order_string[1..].to_s }
|
160
174
|
else
|
161
175
|
{ asc: order_string.to_s }
|
162
176
|
end
|
@@ -199,11 +213,12 @@ module Praxis
|
|
199
213
|
field = field.to_sym
|
200
214
|
next if self.class.fields_allowed.include?(field)
|
201
215
|
|
202
|
-
|
203
|
-
|
216
|
+
field_path = field.to_s.split('.').map(&:to_sym)
|
217
|
+
errors << if valid_attribute_path?(self.class.media_type, field_path)
|
218
|
+
"Ordering by field \'#{field}\' in media type #{self.class.media_type.name} is disallowed. Ordering is only allowed using the following fields: " +
|
204
219
|
self.class.fields_allowed.map { |f| "\'#{f}\'" }.join(', ').to_s
|
205
220
|
else
|
206
|
-
"Ordering by field \'#{field}\' is not possible as this field
|
221
|
+
"Ordering by field \'#{field}\' is not possible as this field is not reachable from " \
|
207
222
|
"media type #{self.class.media_type.name}"
|
208
223
|
end
|
209
224
|
end
|
@@ -226,6 +241,19 @@ module Praxis
|
|
226
241
|
def each(&block)
|
227
242
|
items.each(&block)
|
228
243
|
end
|
244
|
+
|
245
|
+
# Looks up if the given path (with symbol attribute names at each component) is actually
|
246
|
+
# a valid path from the given mediatype
|
247
|
+
def valid_attribute_path?(media_type, path)
|
248
|
+
first, *rest = path
|
249
|
+
# Get the member type if this is a collection
|
250
|
+
media_type = media_type.member_type if media_type.respond_to?(:member_attribute)
|
251
|
+
if (attribute = media_type.attributes[first])
|
252
|
+
rest.empty? ? true : valid_attribute_path?(attribute.type, rest)
|
253
|
+
else
|
254
|
+
false
|
255
|
+
end
|
256
|
+
end
|
229
257
|
end
|
230
258
|
end
|
231
259
|
end
|
@@ -6,7 +6,7 @@ module Praxis
|
|
6
6
|
class PaginationHandler
|
7
7
|
class PaginationException < RuntimeError; end
|
8
8
|
|
9
|
-
def self.paginate(query, pagination)
|
9
|
+
def self.paginate(query, pagination, root_resource:)
|
10
10
|
return query unless pagination.paginator
|
11
11
|
|
12
12
|
paginator = pagination.paginator
|
@@ -18,7 +18,7 @@ module Praxis
|
|
18
18
|
# i.e., We can be smart about allowing the main sort field matching the pagination one (in case you want to sub-order in a custom way)
|
19
19
|
oclause = if pagination.order.nil? || pagination.order.empty? # No ordering specified => use ascending based on the "by" field
|
20
20
|
direction = :asc
|
21
|
-
order(query, [{ asc: paginator.by }])
|
21
|
+
order(query, [{ asc: paginator.by }], root_resource: root_resource)
|
22
22
|
else
|
23
23
|
first_ordering = pagination.order.items.first
|
24
24
|
direction = first_ordering.keys.first
|
@@ -32,7 +32,7 @@ module Praxis
|
|
32
32
|
"When paginating by a field value, one cannot specify the 'order' clause " \
|
33
33
|
"unless the clause's primary field matches the pagination field."
|
34
34
|
end
|
35
|
-
order(query, pagination.order)
|
35
|
+
order(query, pagination.order, root_resource: root_resource)
|
36
36
|
end
|
37
37
|
|
38
38
|
if paginator.from
|