praxis 2.0.pre.17 → 2.0.pre.21
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 +54 -0
- data/.simplecov +3 -1
- data/.travis.yml +2 -1
- data/CHANGELOG.md +19 -0
- data/CONTRIBUTING.md +2 -79
- data/Gemfile +5 -1
- data/Guardfile +6 -4
- data/LICENSE +0 -2
- data/MAINTAINERS.md +1 -0
- data/README.md +15 -22
- data/Rakefile +4 -2
- data/bin/praxis +55 -58
- data/lib/praxis/action_definition/headers_dsl_compiler.rb +5 -6
- data/lib/praxis/action_definition.rb +65 -95
- data/lib/praxis/api_definition.rb +21 -29
- data/lib/praxis/api_general_info.rb +55 -66
- data/lib/praxis/application.rb +15 -32
- data/lib/praxis/blueprint.rb +80 -73
- data/lib/praxis/bootloader.rb +24 -33
- data/lib/praxis/bootloader_stages/environment.rb +5 -10
- data/lib/praxis/bootloader_stages/file_loader.rb +3 -6
- data/lib/praxis/bootloader_stages/plugin_config_load.rb +4 -6
- data/lib/praxis/bootloader_stages/plugin_config_prepare.rb +2 -2
- data/lib/praxis/bootloader_stages/plugin_loader.rb +3 -7
- data/lib/praxis/bootloader_stages/plugin_setup.rb +3 -3
- data/lib/praxis/bootloader_stages/routing.rb +5 -8
- data/lib/praxis/bootloader_stages/subgroup_loader.rb +2 -10
- data/lib/praxis/bootloader_stages/warn_unloaded_files.rb +15 -19
- data/lib/praxis/callbacks.rb +12 -11
- data/lib/praxis/collection.rb +11 -14
- data/lib/praxis/config.rb +17 -28
- data/lib/praxis/config_hash.rb +2 -1
- data/lib/praxis/controller.rb +7 -6
- data/lib/praxis/dispatcher.rb +34 -42
- data/lib/praxis/docs/open_api/info_object.rb +11 -8
- data/lib/praxis/docs/open_api/media_type_object.rb +18 -17
- data/lib/praxis/docs/open_api/operation_object.rb +7 -4
- data/lib/praxis/docs/open_api/parameter_object.rb +17 -14
- data/lib/praxis/docs/open_api/paths_object.rb +11 -9
- data/lib/praxis/docs/open_api/request_body_object.rb +14 -13
- data/lib/praxis/docs/open_api/response_object.rb +24 -18
- data/lib/praxis/docs/open_api/responses_object.rb +3 -1
- data/lib/praxis/docs/open_api/schema_object.rb +61 -29
- data/lib/praxis/docs/open_api/server_object.rb +5 -2
- data/lib/praxis/docs/open_api/tag_object.rb +9 -6
- data/lib/praxis/docs/open_api_generator.rb +114 -150
- data/lib/praxis/endpoint_definition.rb +60 -77
- data/lib/praxis/error_handler.rb +2 -2
- data/lib/praxis/exception.rb +2 -0
- data/lib/praxis/exceptions/config.rb +3 -1
- data/lib/praxis/exceptions/config_load.rb +2 -0
- data/lib/praxis/exceptions/config_validation.rb +3 -1
- data/lib/praxis/exceptions/invalid_configuration.rb +3 -1
- data/lib/praxis/exceptions/invalid_response.rb +3 -1
- data/lib/praxis/exceptions/invalid_trait.rb +3 -1
- data/lib/praxis/exceptions/stage_not_found.rb +3 -1
- data/lib/praxis/exceptions/validation.rb +4 -3
- data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +163 -149
- data/lib/praxis/extensions/attribute_filtering/active_record_patches/5x.rb +18 -13
- data/lib/praxis/extensions/attribute_filtering/active_record_patches/6_0.rb +13 -9
- data/lib/praxis/extensions/attribute_filtering/active_record_patches/6_1_plus.rb +14 -11
- data/lib/praxis/extensions/attribute_filtering/active_record_patches.rb +12 -9
- data/lib/praxis/extensions/attribute_filtering/filter_tree_node.rb +8 -5
- data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +89 -65
- data/lib/praxis/extensions/attribute_filtering/filters_parser.rb +68 -62
- data/lib/praxis/extensions/attribute_filtering.rb +3 -1
- data/lib/praxis/extensions/field_expansion.rb +6 -4
- data/lib/praxis/extensions/field_selection/active_record_query_selector.rb +10 -8
- data/lib/praxis/extensions/field_selection/field_selector.rb +91 -92
- data/lib/praxis/extensions/field_selection/sequel_query_selector.rb +12 -12
- data/lib/praxis/extensions/field_selection.rb +3 -1
- data/lib/praxis/extensions/pagination/active_record_pagination_handler.rb +6 -4
- data/lib/praxis/extensions/pagination/header_generator.rb +16 -11
- data/lib/praxis/extensions/pagination/ordering_params.rb +29 -28
- data/lib/praxis/extensions/pagination/pagination_handler.rb +44 -42
- data/lib/praxis/extensions/pagination/pagination_params.rb +29 -48
- data/lib/praxis/extensions/pagination/sequel_pagination_handler.rb +8 -7
- data/lib/praxis/extensions/pagination.rb +10 -15
- data/lib/praxis/extensions/rails_compat/request_methods.rb +3 -4
- data/lib/praxis/extensions/rails_compat.rb +2 -0
- data/lib/praxis/extensions/rendering.rb +12 -12
- data/lib/praxis/field_expander.rb +8 -9
- data/lib/praxis/file_group.rb +8 -12
- data/lib/praxis/finalizable.rb +1 -0
- data/lib/praxis/handlers/json.rb +5 -2
- data/lib/praxis/handlers/plain.rb +2 -1
- data/lib/praxis/handlers/www_form.rb +6 -3
- data/lib/praxis/handlers/{xml-sample.rb → xml_sample.rb} +26 -22
- data/lib/praxis/mapper/active_model_compat.rb +13 -10
- data/lib/praxis/mapper/resource.rb +196 -181
- data/lib/praxis/mapper/selector_generator.rb +106 -112
- data/lib/praxis/mapper/sequel_compat.rb +70 -67
- data/lib/praxis/media_type.rb +2 -2
- data/lib/praxis/media_type_identifier.rb +26 -22
- data/lib/praxis/middleware_app.rb +18 -15
- data/lib/praxis/multipart/parser.rb +46 -51
- data/lib/praxis/multipart/part.rb +78 -110
- data/lib/praxis/notifications.rb +2 -4
- data/lib/praxis/plugin.rb +11 -18
- data/lib/praxis/plugin_concern.rb +12 -15
- data/lib/praxis/plugins/mapper_plugin.rb +15 -13
- data/lib/praxis/plugins/pagination_plugin.rb +8 -6
- data/lib/praxis/plugins/rails_plugin.rb +33 -28
- data/lib/praxis/renderer.rb +11 -15
- data/lib/praxis/request.rb +48 -44
- data/lib/praxis/request_stages/action.rb +4 -6
- data/lib/praxis/request_stages/load_request.rb +2 -4
- data/lib/praxis/request_stages/request_stage.rb +19 -23
- data/lib/praxis/request_stages/response.rb +4 -6
- data/lib/praxis/request_stages/validate.rb +3 -5
- data/lib/praxis/request_stages/validate_params_and_headers.rb +15 -22
- data/lib/praxis/request_stages/validate_payload.rb +25 -28
- data/lib/praxis/request_superclassing.rb +3 -3
- data/lib/praxis/resource_definition.rb +1 -0
- data/lib/praxis/response.rb +24 -26
- data/lib/praxis/response_definition.rb +77 -122
- data/lib/praxis/response_template.rb +11 -15
- data/lib/praxis/responses/http.rb +23 -44
- data/lib/praxis/responses/internal_server_error.rb +18 -21
- data/lib/praxis/responses/multipart_ok.rb +4 -9
- data/lib/praxis/responses/validation_error.rb +8 -15
- data/lib/praxis/route.rb +8 -10
- data/lib/praxis/router/rack.rb +13 -7
- data/lib/praxis/router/simple.rb +10 -5
- data/lib/praxis/router.rb +27 -34
- data/lib/praxis/routing_config.rb +52 -29
- data/lib/praxis/simple_media_type.rb +5 -8
- data/lib/praxis/stage.rb +17 -25
- data/lib/praxis/tasks/api_docs.rb +17 -16
- data/lib/praxis/tasks/console.rb +3 -1
- data/lib/praxis/tasks/environment.rb +2 -0
- data/lib/praxis/tasks/routes.rb +26 -24
- data/lib/praxis/tasks.rb +3 -1
- data/lib/praxis/trait.rb +37 -46
- data/lib/praxis/types/fuzzy_hash.rb +13 -14
- data/lib/praxis/types/media_type_common.rb +11 -10
- data/lib/praxis/types/multipart_array/part_definition.rb +14 -17
- data/lib/praxis/types/multipart_array.rb +100 -115
- data/lib/praxis/validation_handler.rb +5 -3
- data/lib/praxis/version.rb +3 -1
- data/lib/praxis.rb +4 -5
- data/praxis.gemspec +22 -21
- data/spec/functional_spec.rb +44 -56
- data/spec/praxis/action_definition_spec.rb +39 -48
- data/spec/praxis/api_definition_spec.rb +45 -47
- data/spec/praxis/api_general_info_spec.rb +28 -29
- data/spec/praxis/application_spec.rb +18 -14
- data/spec/praxis/blueprint_spec.rb +33 -34
- data/spec/praxis/bootloader_spec.rb +32 -30
- data/spec/praxis/callbacks_spec.rb +37 -37
- data/spec/praxis/collection_spec.rb +18 -25
- data/spec/praxis/config_hash_spec.rb +5 -4
- data/spec/praxis/config_spec.rb +27 -26
- data/spec/praxis/controller_spec.rb +8 -9
- data/spec/praxis/endpoint_definition_spec.rb +25 -32
- data/spec/praxis/extensions/attribute_filtering/active_record_filter_query_builder_spec.rb +171 -114
- data/spec/praxis/extensions/attribute_filtering/filter_tree_node_spec.rb +22 -21
- data/spec/praxis/extensions/attribute_filtering/filtering_params_spec.rb +112 -60
- data/spec/praxis/extensions/attribute_filtering/filters_parser_spec.rb +37 -38
- data/spec/praxis/extensions/field_expansion_spec.rb +8 -10
- data/spec/praxis/extensions/field_selection/active_record_query_selector_spec.rb +14 -13
- data/spec/praxis/extensions/field_selection/field_selector_spec.rb +9 -16
- data/spec/praxis/extensions/field_selection/sequel_query_selector_spec.rb +50 -49
- data/spec/praxis/extensions/pagination/active_record_pagination_handler_spec.rb +32 -31
- data/spec/praxis/extensions/rendering_spec.rb +9 -9
- data/spec/praxis/extensions/support/spec_resources_active_model.rb +32 -49
- data/spec/praxis/extensions/support/spec_resources_sequel.rb +48 -48
- data/spec/praxis/field_expander_spec.rb +6 -5
- data/spec/praxis/file_group_spec.rb +3 -1
- data/spec/praxis/handlers/json_spec.rb +6 -5
- data/spec/praxis/mapper/resource_spec.rb +39 -29
- data/spec/praxis/mapper/selector_generator_spec.rb +80 -46
- data/spec/praxis/media_type_identifier_spec.rb +13 -10
- data/spec/praxis/media_type_spec.rb +12 -12
- data/spec/praxis/middleware_app_spec.rb +23 -22
- data/spec/praxis/multipart/parser_spec.rb +7 -9
- data/spec/praxis/notifications_spec.rb +4 -4
- data/spec/praxis/plugin_concern_spec.rb +5 -6
- data/spec/praxis/renderer_spec.rb +10 -9
- data/spec/praxis/request_spec.rb +38 -41
- data/spec/praxis/request_stages/action_spec.rb +14 -15
- data/spec/praxis/request_stages/request_stage_spec.rb +30 -41
- data/spec/praxis/request_stages/validate_spec.rb +3 -1
- data/spec/praxis/response_definition_spec.rb +79 -92
- data/spec/praxis/response_spec.rb +35 -40
- data/spec/praxis/responses/internal_server_error_spec.rb +6 -9
- data/spec/praxis/responses/validation_error_spec.rb +17 -18
- data/spec/praxis/route_spec.rb +4 -7
- data/spec/praxis/router_spec.rb +69 -79
- data/spec/praxis/routing_config_spec.rb +15 -14
- data/spec/praxis/stage_spec.rb +56 -53
- data/spec/praxis/trait_spec.rb +17 -17
- data/spec/praxis/types/fuzzy_hash_spec.rb +11 -9
- data/spec/praxis/types/multipart_array/part_definition_spec.rb +3 -2
- data/spec/praxis/types/multipart_array_spec.rb +33 -48
- data/spec/spec_app/app/concerns/authenticated.rb +5 -5
- data/spec/spec_app/app/concerns/basic_api.rb +3 -1
- data/spec/spec_app/app/concerns/log_wrapper.rb +5 -3
- data/spec/spec_app/app/controllers/base_class.rb +6 -5
- data/spec/spec_app/app/controllers/instances.rb +31 -34
- data/spec/spec_app/app/controllers/volumes.rb +6 -6
- data/spec/spec_app/app/responses/multipart.rb +1 -2
- data/spec/spec_app/app/responses/other_response.rb +2 -2
- data/spec/spec_app/config/environment.rb +19 -6
- data/spec/spec_app/config.ru +4 -3
- data/spec/spec_app/design/api.rb +13 -15
- data/spec/spec_app/design/media_types/instance.rb +6 -6
- data/spec/spec_app/design/media_types/volume.rb +2 -1
- data/spec/spec_app/design/media_types/volume_snapshot.rb +2 -1
- data/spec/spec_app/design/resources/instances.rb +11 -17
- data/spec/spec_app/design/resources/volume_snapshots.rb +4 -5
- data/spec/spec_app/design/resources/volumes.rb +4 -5
- data/spec/spec_helper.rb +11 -13
- data/spec/support/be_deep_equal_matcher.rb +5 -0
- data/spec/support/spec_authorization_plugin.rb +7 -12
- data/spec/support/spec_blueprints.rb +5 -4
- data/spec/support/spec_complex_authentication_plugin.rb +17 -34
- data/spec/support/spec_endpoint_definitions.rb +2 -3
- data/spec/support/spec_media_types.rb +28 -35
- data/spec/support/spec_resources.rb +22 -16
- data/spec/support/spec_simple_authentication_plugin.rb +5 -9
- data/tasks/loader.thor +4 -2
- data/tasks/thor/app.rb +7 -5
- data/tasks/thor/example.rb +23 -22
- data/tasks/thor/model.rb +7 -7
- data/tasks/thor/scaffold.rb +23 -23
- data/tasks/thor/templates/generator/example_app/app/v1/resources/user.rb +0 -8
- data/tasks/thor/templates/generator/scaffold/implementation/resources/item.rb +1 -2
- metadata +72 -84
- data/MAINTAINERS +0 -2
- data/TODO.md +0 -25
- data/spec/praxis/api_resource_spec.rb +0 -0
- data/spec/praxis/dispatcher_spec.rb +0 -0
- data/spec/spec_app/app/responses/bulk_response.rb +0 -0
|
@@ -1,30 +1,32 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
module Praxis
|
|
3
4
|
module Extensions
|
|
4
5
|
module FieldSelection
|
|
5
6
|
class ActiveRecordQuerySelector
|
|
6
7
|
attr_reader :selector, :query
|
|
8
|
+
|
|
7
9
|
# Gets a dataset, a selector...and should return a dataset with the selector definition applied.
|
|
8
10
|
def initialize(query:, selectors:, debug: false)
|
|
9
11
|
@selector = selectors
|
|
10
12
|
@query = query
|
|
11
|
-
@logger = debug ? Logger.new(
|
|
13
|
+
@logger = debug ? Logger.new($stdout) : nil
|
|
12
14
|
end
|
|
13
15
|
|
|
14
16
|
def generate
|
|
15
|
-
# TODO: unfortunately, I think we can only control the select clauses for the top model
|
|
17
|
+
# TODO: unfortunately, I think we can only control the select clauses for the top model
|
|
16
18
|
# (as I'm not sure ActiveRecord supports expressing it in the join...)
|
|
17
19
|
@query = add_select(query: query, selector_node: selector)
|
|
18
20
|
eager_hash = _eager(selector)
|
|
19
21
|
|
|
20
|
-
@query = @query.includes(eager_hash)
|
|
22
|
+
@query = @query.includes(eager_hash)
|
|
21
23
|
explain_query(query, eager_hash) if @logger
|
|
22
24
|
|
|
23
25
|
@query
|
|
24
26
|
end
|
|
25
27
|
|
|
26
28
|
def add_select(query:, selector_node:)
|
|
27
|
-
# We're gonna always require the PK of the model, as it is a special case for AR, and the app itself
|
|
29
|
+
# We're gonna always require the PK of the model, as it is a special case for AR, and the app itself
|
|
28
30
|
# might assume it is always there and not be surprised by the fact that if it isn't, it won't blow up
|
|
29
31
|
# in the same way as any other attribute not being loaded...i.e., ActiveModel::MissingAttributeError: missing attribute: xyz
|
|
30
32
|
select_fields = selector_node.select + [selector_node.resource.model.primary_key.to_sym]
|
|
@@ -32,8 +34,8 @@ module Praxis
|
|
|
32
34
|
end
|
|
33
35
|
|
|
34
36
|
def _eager(selector_node)
|
|
35
|
-
selector_node.tracks.
|
|
36
|
-
|
|
37
|
+
selector_node.tracks.transform_values do |track_node|
|
|
38
|
+
_eager(track_node)
|
|
37
39
|
end
|
|
38
40
|
end
|
|
39
41
|
|
|
@@ -41,9 +43,9 @@ module Praxis
|
|
|
41
43
|
@logger.debug("Query plan for ...#{selector.resource.model} with selectors: #{JSON.generate(selector.dump)}")
|
|
42
44
|
@logger.debug(" ActiveRecord query: #{selector.resource.model}.includes(#{eager_hash})")
|
|
43
45
|
query.explain
|
|
44
|
-
@logger.debug(
|
|
46
|
+
@logger.debug('Query plan end')
|
|
45
47
|
end
|
|
46
48
|
end
|
|
47
49
|
end
|
|
48
50
|
end
|
|
49
|
-
end
|
|
51
|
+
end
|
|
@@ -1,123 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
|
|
2
3
|
module Praxis
|
|
3
4
|
module Extensions
|
|
4
5
|
module FieldSelection
|
|
6
|
+
class FieldSelector
|
|
7
|
+
include Attributor::Type
|
|
8
|
+
include Attributor::Dumpable
|
|
5
9
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
10
|
+
def self.json_schema_type
|
|
11
|
+
:string
|
|
12
|
+
end
|
|
9
13
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def self.native_type
|
|
15
|
-
self
|
|
16
|
-
end
|
|
14
|
+
def self.native_type
|
|
15
|
+
self
|
|
16
|
+
end
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
def self.display_name
|
|
19
|
+
'FieldSelector'
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.family
|
|
23
|
+
'string'
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.for(media_type)
|
|
27
|
+
unless media_type < Praxis::MediaType
|
|
28
|
+
raise ArgumentError, "Invalid type: #{media_type.name} for FieldSelector. " \
|
|
29
|
+
'Must be a subclass of MediaType'
|
|
20
30
|
end
|
|
21
31
|
|
|
22
|
-
|
|
23
|
-
|
|
32
|
+
::Class.new(self) do
|
|
33
|
+
@media_type = media_type
|
|
24
34
|
end
|
|
35
|
+
end
|
|
25
36
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
raise ArgumentError, "Invalid type: #{media_type.name} for FieldSelector. " +
|
|
29
|
-
"Must be a subclass of MediaType"
|
|
30
|
-
end
|
|
37
|
+
def self.load(value, _context = Attributor::DEFAULT_ROOT_CONTEXT, **_options)
|
|
38
|
+
return value if value.is_a?(native_type)
|
|
31
39
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
40
|
+
if value.nil? || value.blank?
|
|
41
|
+
new(true)
|
|
42
|
+
else
|
|
43
|
+
parsed = Attributor::FieldSelector.load(value)
|
|
44
|
+
new(parsed)
|
|
35
45
|
end
|
|
46
|
+
end
|
|
36
47
|
|
|
37
|
-
|
|
38
|
-
|
|
48
|
+
def self.example(context = Attributor::DEFAULT_ROOT_CONTEXT, **options)
|
|
49
|
+
fields = if media_type
|
|
50
|
+
media_type.attributes.keys.sample(3).join(',')
|
|
51
|
+
else
|
|
52
|
+
Attributor::FieldSelector.example(context, **options)
|
|
53
|
+
end
|
|
54
|
+
self.load(fields)
|
|
55
|
+
end
|
|
39
56
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
else
|
|
43
|
-
parsed = Attributor::FieldSelector.load(value)
|
|
44
|
-
self.new(parsed)
|
|
45
|
-
end
|
|
46
|
-
end
|
|
57
|
+
def self.validate(value, context = Attributor::DEFAULT_ROOT_CONTEXT, _attribute = nil)
|
|
58
|
+
return [] unless media_type
|
|
47
59
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
else
|
|
52
|
-
Attributor::FieldSelector.example(context,**options)
|
|
53
|
-
end
|
|
54
|
-
self.load(fields)
|
|
55
|
-
end
|
|
60
|
+
instance = self.load(value, context)
|
|
61
|
+
instance.validate(context)
|
|
62
|
+
end
|
|
56
63
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
instance.validate(context)
|
|
61
|
-
end
|
|
64
|
+
def self.dump(value, **_opts)
|
|
65
|
+
self.load(value).dump
|
|
66
|
+
end
|
|
62
67
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
68
|
+
class << self
|
|
69
|
+
attr_reader :media_type
|
|
70
|
+
end
|
|
66
71
|
|
|
67
|
-
|
|
68
|
-
attr_reader :media_type
|
|
69
|
-
end
|
|
72
|
+
attr_reader :fields
|
|
70
73
|
|
|
71
|
-
|
|
74
|
+
def initialize(fields)
|
|
75
|
+
@fields = fields
|
|
76
|
+
end
|
|
72
77
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
end
|
|
78
|
+
def dump(*_args)
|
|
79
|
+
return '' if fields == true
|
|
76
80
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
_dump(self.fields)
|
|
80
|
-
end
|
|
81
|
+
_dump(fields)
|
|
82
|
+
end
|
|
81
83
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
84
|
+
def _dump(fields)
|
|
85
|
+
fields.each_with_object([]) do |(field, spec), array|
|
|
86
|
+
array << if spec == true
|
|
87
|
+
field
|
|
88
|
+
else
|
|
89
|
+
"#{field}{#{_dump(spec)}}"
|
|
90
|
+
end
|
|
91
|
+
end.join(',')
|
|
92
|
+
end
|
|
91
93
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
def validate(_context = Attributor::DEFAULT_ROOT_CONTEXT)
|
|
95
|
+
errors = []
|
|
96
|
+
return errors if fields == true
|
|
97
|
+
|
|
98
|
+
_validate(self.class.media_type, fields)
|
|
99
|
+
end
|
|
97
100
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
if field_spec.kind_of?(Hash)
|
|
107
|
-
sub_context = context + [name]
|
|
108
|
-
sub_attribute = type.attributes[name]
|
|
109
|
-
sub_type = sub_attribute.type
|
|
110
|
-
if sub_attribute.type.respond_to?(:member_attribute)
|
|
111
|
-
sub_type = sub_type.member_type
|
|
112
|
-
end
|
|
113
|
-
errors.push(*_validate(sub_type,field_spec, sub_context))
|
|
114
|
-
end
|
|
101
|
+
def _validate(type, fields, context = Attributor::DEFAULT_ROOT_CONTEXT)
|
|
102
|
+
errors = []
|
|
103
|
+
fields.each do |name, field_spec|
|
|
104
|
+
unless type.attributes.key?(name)
|
|
105
|
+
errors << "Attribute with name #{name} not found in #{Attributor.type_name(type)}"
|
|
106
|
+
next
|
|
115
107
|
end
|
|
116
|
-
errors
|
|
117
|
-
end
|
|
118
108
|
|
|
119
|
-
|
|
109
|
+
next unless field_spec.is_a?(Hash)
|
|
120
110
|
|
|
111
|
+
sub_context = context + [name]
|
|
112
|
+
sub_attribute = type.attributes[name]
|
|
113
|
+
sub_type = sub_attribute.type
|
|
114
|
+
sub_type = sub_type.member_type if sub_attribute.type.respond_to?(:member_attribute)
|
|
115
|
+
errors.push(*_validate(sub_type, field_spec, sub_context))
|
|
116
|
+
end
|
|
117
|
+
errors
|
|
118
|
+
end
|
|
119
|
+
end
|
|
121
120
|
end
|
|
122
121
|
end
|
|
123
122
|
end
|
|
@@ -7,18 +7,19 @@ module Praxis
|
|
|
7
7
|
module FieldSelection
|
|
8
8
|
class SequelQuerySelector
|
|
9
9
|
attr_reader :selector, :query
|
|
10
|
+
|
|
10
11
|
# Gets a dataset, a selector...and should return a dataset with the selector definition applied.
|
|
11
12
|
def initialize(query:, selectors:, debug: false)
|
|
12
13
|
@selector = selectors
|
|
13
14
|
@query = query
|
|
14
|
-
@logger = debug ? Logger.new(
|
|
15
|
+
@logger = debug ? Logger.new($stdout) : nil
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
def generate
|
|
18
19
|
@query = add_select(query: query, selector_node: @selector)
|
|
19
|
-
|
|
20
|
+
|
|
20
21
|
@query = @selector.tracks.inject(@query) do |ds, (track_name, track_node)|
|
|
21
|
-
ds.eager(track_name => _eager(track_node)
|
|
22
|
+
ds.eager(track_name => _eager(track_node))
|
|
22
23
|
end
|
|
23
24
|
|
|
24
25
|
explain_query(query) if @logger
|
|
@@ -29,30 +30,29 @@ module Praxis
|
|
|
29
30
|
lambda do |dset|
|
|
30
31
|
dset = add_select(query: dset, selector_node: selector_node)
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
ds.eager(track_name => _eager(track_node)
|
|
33
|
+
selector_node.tracks.inject(dset) do |ds, (track_name, track_node)|
|
|
34
|
+
ds.eager(track_name => _eager(track_node))
|
|
34
35
|
end
|
|
35
|
-
|
|
36
36
|
end
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
def add_select(query:, selector_node:)
|
|
40
|
-
# We're gonna always require the PK of the model, as it is a special case for Sequel, and the app itself
|
|
40
|
+
# We're gonna always require the PK of the model, as it is a special case for Sequel, and the app itself
|
|
41
41
|
# might assume it is always there and not be surprised by the fact that if it isn't, it won't blow up
|
|
42
42
|
# in the same way as any other attribute not being loaded...i.e., NoMethodError: undefined method `foobar' for #<...>
|
|
43
43
|
select_fields = selector_node.select + [selector_node.resource.model.primary_key.to_sym]
|
|
44
|
-
|
|
44
|
+
|
|
45
45
|
table_name = selector_node.resource.model.table_name
|
|
46
46
|
qualified = select_fields.map { |f| Sequel.qualify(table_name, f) }
|
|
47
47
|
query.select(*qualified)
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
-
def explain_query(
|
|
50
|
+
def explain_query(dataset)
|
|
51
51
|
@logger.debug("Query plan for ...#{selector.resource.model} with selectors: #{JSON.generate(selector.dump)}")
|
|
52
|
-
|
|
53
|
-
@logger.debug(
|
|
52
|
+
dataset.all
|
|
53
|
+
@logger.debug('Query plan end')
|
|
54
54
|
end
|
|
55
55
|
end
|
|
56
56
|
end
|
|
57
57
|
end
|
|
58
|
-
end
|
|
58
|
+
end
|
|
@@ -1,30 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require_relative 'pagination_handler'
|
|
2
4
|
|
|
3
5
|
module Praxis
|
|
4
6
|
module Extensions
|
|
5
7
|
module Pagination
|
|
6
8
|
class ActiveRecordPaginationHandler < PaginationHandler
|
|
7
|
-
|
|
8
9
|
def self.where_lt(query, attr, value)
|
|
9
10
|
# TODO: common for AR/Sequel? Seems we could use Arel and more-specific Sequel things
|
|
10
11
|
query.where(query.table[attr].lt(value))
|
|
11
12
|
end
|
|
12
|
-
|
|
13
|
+
|
|
13
14
|
def self.where_gt(query, attr, value)
|
|
14
15
|
query.where(query.table[attr].gt(value))
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
def self.order(query, order)
|
|
18
19
|
return query unless order
|
|
20
|
+
|
|
19
21
|
query = query.reorder('')
|
|
20
|
-
|
|
22
|
+
|
|
21
23
|
order.each do |spec_hash|
|
|
22
24
|
direction, name = spec_hash.first
|
|
23
25
|
query = query.order(name => direction)
|
|
24
26
|
end
|
|
25
27
|
query
|
|
26
28
|
end
|
|
27
|
-
|
|
29
|
+
|
|
28
30
|
def self.count(query)
|
|
29
31
|
query.count(:all)
|
|
30
32
|
end
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Praxis
|
|
2
4
|
module Extensions
|
|
3
5
|
module Pagination
|
|
4
|
-
class HeaderGenerator
|
|
6
|
+
class HeaderGenerator
|
|
5
7
|
def self.build_cursor_headers(paginator:, last_value:, total_count: nil)
|
|
6
|
-
[
|
|
8
|
+
%i[next prev first last].each_with_object({}) do |rel_name, info|
|
|
7
9
|
case rel_name
|
|
8
10
|
when :next
|
|
9
11
|
# If we don't know the total, we'll try to go to the next page
|
|
@@ -26,26 +28,29 @@ module Praxis
|
|
|
26
28
|
# This is only for plain paging
|
|
27
29
|
def self.build_paging_headers(paginator:, total_count: nil)
|
|
28
30
|
last_page = total_count.nil? ? nil : (total_count / (paginator.items * 1.0)).ceil
|
|
29
|
-
[
|
|
31
|
+
%i[next prev first last].each_with_object({}) do |rel_name, info|
|
|
30
32
|
num = case rel_name
|
|
31
33
|
when :first
|
|
32
34
|
1
|
|
33
35
|
when :prev
|
|
34
36
|
next if paginator.page < 2
|
|
37
|
+
|
|
35
38
|
paginator.page - 1
|
|
36
39
|
when :next
|
|
37
40
|
# don't include this link if we know the total and we see there are no more pages
|
|
38
41
|
next if last_page.present? && (paginator.page >= last_page)
|
|
42
|
+
|
|
39
43
|
# if we don't know the total, we'll specify to the next page even if it ends up being blank
|
|
40
44
|
paginator.page + 1
|
|
41
45
|
when :last
|
|
42
46
|
next if last_page.blank?
|
|
47
|
+
|
|
43
48
|
last_page
|
|
44
49
|
end
|
|
45
50
|
info[rel_name] = {
|
|
46
|
-
page:
|
|
47
|
-
items:
|
|
48
|
-
total_count:
|
|
51
|
+
page: num,
|
|
52
|
+
items: paginator.items,
|
|
53
|
+
total_count: total_count.present?
|
|
49
54
|
}
|
|
50
55
|
end
|
|
51
56
|
end
|
|
@@ -53,15 +58,15 @@ module Praxis
|
|
|
53
58
|
def self.generate_headers(links:, current_url:, current_query_params:, total_count:)
|
|
54
59
|
mapped = links.map do |(rel, info)|
|
|
55
60
|
# Make sure to encode it our way (with comma-separated args, as it is our own syntax, and not a query string one)
|
|
56
|
-
pagination_param = info.map { |(k, v)| "#{k}=#{v}" }.join(
|
|
57
|
-
new_url = current_url
|
|
61
|
+
pagination_param = info.map { |(k, v)| "#{k}=#{v}" }.join(',')
|
|
62
|
+
new_url = "#{current_url}?#{current_query_params.dup.merge(pagination: pagination_param).to_query}"
|
|
58
63
|
|
|
59
|
-
LinkHeader::Link.new(new_url, [[
|
|
64
|
+
LinkHeader::Link.new(new_url, [['rel', rel.to_s]])
|
|
60
65
|
end
|
|
61
66
|
link_header = LinkHeader.new(mapped)
|
|
62
67
|
|
|
63
|
-
headers = {
|
|
64
|
-
headers[
|
|
68
|
+
headers = { 'Link' => link_header.to_s }
|
|
69
|
+
headers['Total-Count'] = total_count if total_count
|
|
65
70
|
headers
|
|
66
71
|
end
|
|
67
72
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'forwardable'
|
|
2
4
|
|
|
3
5
|
module Praxis
|
|
@@ -65,13 +67,12 @@ module Praxis
|
|
|
65
67
|
# MediaType, so that the field name checking and value coercion can be performed
|
|
66
68
|
class << self
|
|
67
69
|
attr_reader :media_type
|
|
68
|
-
attr_accessor :fields_allowed
|
|
69
|
-
attr_accessor :enforce_all # True when we need to enforce the allowed fields at all ordering positions
|
|
70
|
+
attr_accessor :fields_allowed, :enforce_all # True when we need to enforce the allowed fields at all ordering positions
|
|
70
71
|
|
|
71
72
|
def for(media_type, **_opts)
|
|
72
73
|
unless media_type < Praxis::MediaType
|
|
73
74
|
raise ArgumentError, "Invalid type: #{media_type.name} for Ordering. " \
|
|
74
|
-
|
|
75
|
+
'Must be a subclass of MediaType'
|
|
75
76
|
end
|
|
76
77
|
|
|
77
78
|
::Class.new(self) do
|
|
@@ -121,22 +122,22 @@ module Praxis
|
|
|
121
122
|
|
|
122
123
|
def self.example(_context = Attributor::DEFAULT_ROOT_CONTEXT, **_options)
|
|
123
124
|
fields = if media_type
|
|
124
|
-
|
|
125
|
+
chosen_set = if enforce_all
|
|
125
126
|
fields_allowed.sample(2)
|
|
126
127
|
else
|
|
127
128
|
starting_set = fields_allowed.sample(1)
|
|
128
129
|
simple_attrs = media_type.attributes.select do |_k, attr|
|
|
129
130
|
attr.type == Attributor::String || attr.type < Attributor::Numeric || attr.type < Attributor::Temporal
|
|
130
131
|
end.keys
|
|
131
|
-
starting_set + simple_attrs.
|
|
132
|
+
starting_set + simple_attrs.reject { |attr| attr == starting_set.first }.sample(1)
|
|
132
133
|
end
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
134
|
+
chosen_set.each_with_object([]) do |chosen, arr|
|
|
135
|
+
sign = rand(10) < 5 ? '-' : ''
|
|
136
|
+
arr << "#{sign}#{chosen}"
|
|
137
|
+
end.join(',')
|
|
138
|
+
else
|
|
139
|
+
'name,last_name,-birth_date'
|
|
140
|
+
end
|
|
140
141
|
load(fields)
|
|
141
142
|
end
|
|
142
143
|
|
|
@@ -151,13 +152,14 @@ module Praxis
|
|
|
151
152
|
parsed_order = {}
|
|
152
153
|
unless order.nil?
|
|
153
154
|
parsed_order = order.split(',').each_with_object([]) do |order_string, arr|
|
|
154
|
-
item =
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
155
|
+
item = case order_string[0]
|
|
156
|
+
when '-'
|
|
157
|
+
{ desc: order_string[1..-1].to_s }
|
|
158
|
+
when '+'
|
|
159
|
+
{ asc: order_string[1..-1].to_s }
|
|
160
|
+
else
|
|
161
|
+
{ asc: order_string.to_s }
|
|
162
|
+
end
|
|
161
163
|
arr.push item
|
|
162
164
|
end
|
|
163
165
|
end
|
|
@@ -195,7 +197,8 @@ module Praxis
|
|
|
195
197
|
enforceable_items.each do |spec|
|
|
196
198
|
_dir, field = spec.first
|
|
197
199
|
field = field.to_sym
|
|
198
|
-
next
|
|
200
|
+
next if self.class.fields_allowed.include?(field)
|
|
201
|
+
|
|
199
202
|
errors << if self.class.media_type.attributes.key?(field)
|
|
200
203
|
"Ordering by field \'#{field}\' is disallowed. Ordering is only allowed using the following fields: " +
|
|
201
204
|
self.class.fields_allowed.map { |f| "\'#{f}\'" }.join(', ').to_s
|
|
@@ -213,17 +216,15 @@ module Praxis
|
|
|
213
216
|
items.each_with_object([]) do |spec, arr|
|
|
214
217
|
dir, field = spec.first
|
|
215
218
|
arr << if dir == :desc
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
219
|
+
"-#{field}"
|
|
220
|
+
else
|
|
221
|
+
field
|
|
222
|
+
end
|
|
220
223
|
end.join(',')
|
|
221
224
|
end
|
|
222
225
|
|
|
223
|
-
def each
|
|
224
|
-
items.each
|
|
225
|
-
yield item
|
|
226
|
-
end
|
|
226
|
+
def each(&block)
|
|
227
|
+
items.each(&block)
|
|
227
228
|
end
|
|
228
229
|
end
|
|
229
230
|
end
|