praxis 0.22.pre.1 → 2.0.pre.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.travis.yml +5 -20
- data/CHANGELOG.md +328 -323
- data/lib/praxis.rb +13 -9
- data/lib/praxis/action_definition.rb +8 -10
- data/lib/praxis/action_definition/headers_dsl_compiler.rb +1 -1
- data/lib/praxis/api_definition.rb +27 -44
- data/lib/praxis/api_general_info.rb +2 -3
- data/lib/praxis/application.rb +15 -142
- data/lib/praxis/bootloader.rb +1 -2
- data/lib/praxis/bootloader_stages/environment.rb +13 -0
- data/lib/praxis/config.rb +1 -1
- data/lib/praxis/controller.rb +0 -2
- data/lib/praxis/dispatcher.rb +4 -6
- data/lib/praxis/docs/generator.rb +8 -18
- data/lib/praxis/docs/link_builder.rb +1 -1
- data/lib/praxis/error_handler.rb +5 -5
- data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +1 -1
- data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +1 -1
- data/lib/praxis/extensions/attribute_filtering/sequel_filter_query_builder.rb +125 -0
- data/lib/praxis/extensions/field_selection.rb +1 -12
- data/lib/praxis/extensions/field_selection/active_record_query_selector.rb +28 -34
- data/lib/praxis/extensions/field_selection/sequel_query_selector.rb +35 -39
- data/lib/praxis/extensions/rendering.rb +1 -1
- data/lib/praxis/file_group.rb +1 -1
- data/lib/praxis/handlers/xml.rb +1 -1
- data/lib/praxis/mapper/active_model_compat.rb +98 -0
- data/lib/praxis/mapper/resource.rb +242 -0
- data/lib/praxis/mapper/selector_generator.rb +154 -0
- data/lib/praxis/mapper/sequel_compat.rb +76 -0
- data/lib/praxis/media_type_identifier.rb +2 -1
- data/lib/praxis/middleware_app.rb +13 -15
- data/lib/praxis/multipart/part.rb +3 -5
- data/lib/praxis/notifications.rb +1 -1
- data/lib/praxis/plugins/mapper_plugin.rb +64 -0
- data/lib/praxis/request.rb +14 -7
- data/lib/praxis/request_stages/response.rb +2 -3
- data/lib/praxis/resource_definition.rb +15 -19
- data/lib/praxis/response.rb +6 -5
- data/lib/praxis/response_definition.rb +5 -7
- data/lib/praxis/response_template.rb +3 -4
- data/lib/praxis/responses/http.rb +36 -0
- data/lib/praxis/responses/internal_server_error.rb +12 -3
- data/lib/praxis/responses/multipart_ok.rb +11 -4
- data/lib/praxis/responses/validation_error.rb +10 -1
- data/lib/praxis/route.rb +1 -1
- data/lib/praxis/router.rb +3 -3
- data/lib/praxis/routing_config.rb +1 -1
- data/lib/praxis/tasks/api_docs.rb +2 -10
- data/lib/praxis/tasks/routes.rb +0 -1
- data/lib/praxis/trait.rb +1 -1
- data/lib/praxis/types/media_type_common.rb +2 -2
- data/lib/praxis/types/multipart.rb +1 -1
- data/lib/praxis/types/multipart_array.rb +2 -2
- data/lib/praxis/types/multipart_array/part_definition.rb +1 -1
- data/lib/praxis/version.rb +1 -1
- data/praxis.gemspec +11 -9
- data/spec/functional_spec.rb +0 -1
- data/spec/praxis/action_definition_spec.rb +16 -27
- data/spec/praxis/api_definition_spec.rb +8 -13
- data/spec/praxis/api_general_info_spec.rb +8 -3
- data/spec/praxis/application_spec.rb +8 -14
- data/spec/praxis/collection_spec.rb +3 -2
- data/spec/praxis/config_spec.rb +2 -2
- data/spec/praxis/extensions/field_selection/active_record_query_selector_spec.rb +106 -0
- data/spec/praxis/extensions/field_selection/sequel_query_selector_spec.rb +147 -0
- data/spec/praxis/extensions/field_selection/support/spec_resources_active_model.rb +130 -0
- data/spec/praxis/extensions/field_selection/support/spec_resources_sequel.rb +106 -0
- data/spec/praxis/handlers/xml_spec.rb +2 -2
- data/spec/praxis/mapper/resource_spec.rb +169 -0
- data/spec/praxis/mapper/selector_generator_spec.rb +325 -0
- data/spec/praxis/media_type_spec.rb +0 -10
- data/spec/praxis/middleware_app_spec.rb +16 -10
- data/spec/praxis/request_spec.rb +7 -17
- data/spec/praxis/request_stages/action_spec.rb +8 -1
- data/spec/praxis/request_stages/validate_spec.rb +1 -1
- data/spec/praxis/resource_definition_spec.rb +10 -12
- data/spec/praxis/response_definition_spec.rb +12 -26
- data/spec/praxis/response_spec.rb +6 -13
- data/spec/praxis/responses/internal_server_error_spec.rb +5 -2
- data/spec/praxis/router_spec.rb +5 -9
- data/spec/spec_app/app/controllers/instances.rb +1 -1
- data/spec/spec_app/config.ru +6 -1
- data/spec/spec_app/config/environment.rb +3 -21
- data/spec/spec_helper.rb +13 -17
- data/spec/support/be_deep_equal_matcher.rb +39 -0
- data/spec/support/spec_resources.rb +124 -0
- metadata +74 -53
- data/lib/praxis/extensions/attribute_filtering.rb +0 -28
- data/lib/praxis/extensions/attribute_filtering/query_builder.rb +0 -39
- data/lib/praxis/extensions/mapper_selectors.rb +0 -16
- data/lib/praxis/media_type_collection.rb +0 -127
- data/lib/praxis/plugins/praxis_mapper_plugin.rb +0 -246
- data/spec/praxis/media_type_collection_spec.rb +0 -157
- data/spec/praxis/plugins/praxis_mapper_plugin_spec.rb +0 -142
- data/spec/spec_app/app/models/person.rb +0 -3
data/lib/praxis/bootloader.rb
CHANGED
@@ -47,9 +47,8 @@ module Praxis
|
|
47
47
|
stages << BootloaderStages::WarnUnloadedFiles.new(:warn_unloaded_files, application)
|
48
48
|
|
49
49
|
after(:app) do
|
50
|
-
Praxis::Mapper.finalize!
|
51
50
|
Praxis::Blueprint.finalize!
|
52
|
-
Praxis::ResourceDefinition.finalize!
|
51
|
+
Praxis::ResourceDefinition.finalize!
|
53
52
|
end
|
54
53
|
|
55
54
|
end
|
@@ -8,6 +8,7 @@ module Praxis
|
|
8
8
|
# 1) the environment.rb file - generic stuff for all environments
|
9
9
|
# 2) "Deployer.environment".rb - environment specific stuff
|
10
10
|
def execute
|
11
|
+
setup_initial_config!
|
11
12
|
|
12
13
|
env_file = application.root + "config/environment.rb"
|
13
14
|
require env_file if File.exists? env_file
|
@@ -36,6 +37,18 @@ module Praxis
|
|
36
37
|
end
|
37
38
|
end
|
38
39
|
|
40
|
+
# TODO: not really sure I like this here... but where else is better?
|
41
|
+
def setup_initial_config!
|
42
|
+
application.config do
|
43
|
+
attribute :praxis do
|
44
|
+
attribute :validate_responses, Attributor::Boolean, default: false
|
45
|
+
attribute :validate_response_bodies, Attributor::Boolean, default: false
|
46
|
+
|
47
|
+
attribute :show_exceptions, Attributor::Boolean, default: false
|
48
|
+
attribute :x_cascade, Attributor::Boolean, default: true
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
39
52
|
|
40
53
|
end
|
41
54
|
|
data/lib/praxis/config.rb
CHANGED
data/lib/praxis/controller.rb
CHANGED
@@ -20,8 +20,6 @@ module Praxis
|
|
20
20
|
end
|
21
21
|
|
22
22
|
definition.controller = self
|
23
|
-
# `implements` should only be processed while the application initializes/setup
|
24
|
-
# So we will use the `.instance` function to get the "current" application instance
|
25
23
|
Application.instance.controllers << self
|
26
24
|
end
|
27
25
|
|
data/lib/praxis/dispatcher.rb
CHANGED
@@ -28,13 +28,11 @@ module Praxis
|
|
28
28
|
@deferred_callbacks[:after] << [conditions, block]
|
29
29
|
end
|
30
30
|
|
31
|
-
|
32
|
-
# But we'll leave the application param as optional if we know there is a dispatcher in the thread
|
33
|
-
def self.current(thread: Thread.current, application: nil)
|
31
|
+
def self.current(thread: Thread.current, application: Application.instance)
|
34
32
|
thread[:praxis_dispatcher] ||= self.new(application: application)
|
35
33
|
end
|
36
34
|
|
37
|
-
def initialize(application:)
|
35
|
+
def initialize(application: Application.instance)
|
38
36
|
@stages = []
|
39
37
|
@application = application
|
40
38
|
setup_stages!
|
@@ -106,9 +104,9 @@ module Praxis
|
|
106
104
|
response_stage.run
|
107
105
|
|
108
106
|
payload[:response] = controller.response
|
109
|
-
controller.response.finish
|
107
|
+
controller.response.finish
|
110
108
|
rescue => e
|
111
|
-
@application.error_handler.handle!(request, e
|
109
|
+
@application.error_handler.handle!(request, e)
|
112
110
|
end
|
113
111
|
end
|
114
112
|
end
|
@@ -5,8 +5,7 @@ module Praxis
|
|
5
5
|
require 'active_support/core_ext/enumerable' # For index_by
|
6
6
|
|
7
7
|
API_DOCS_DIRNAME = 'docs/api'
|
8
|
-
|
9
|
-
attr_reader :app_instance
|
8
|
+
|
10
9
|
attr_reader :resources_by_version, :types_by_id, :infos_by_version
|
11
10
|
attr_reader :doc_root_dir
|
12
11
|
|
@@ -25,21 +24,13 @@ module Praxis
|
|
25
24
|
Attributor::URI,
|
26
25
|
]).freeze
|
27
26
|
|
28
|
-
|
29
|
-
|
30
|
-
Thread.current[:praxis_instance] = instance
|
31
|
-
self.new(root, instance: instance, name: name, skip_sub_directory: skip_sub_directory).save!
|
32
|
-
Thread.current[:praxis_instance] = nil
|
33
|
-
end
|
34
|
-
|
35
|
-
def initialize(root, instance:, name:, skip_sub_directory:)
|
27
|
+
|
28
|
+
def initialize(root)
|
36
29
|
require 'yaml'
|
37
30
|
@resources_by_version = Hash.new do |h,k|
|
38
31
|
h[k] = Set.new
|
39
32
|
end
|
40
|
-
|
41
|
-
subdir = skip_sub_directory ? nil : name
|
42
|
-
initialize_directories(root, subdir: subdir )
|
33
|
+
initialize_directories(root)
|
43
34
|
|
44
35
|
Attributor::AttributeResolver.current = Attributor::AttributeResolver.new
|
45
36
|
collect_infos
|
@@ -57,10 +48,9 @@ module Praxis
|
|
57
48
|
|
58
49
|
private
|
59
50
|
|
60
|
-
def initialize_directories(root
|
51
|
+
def initialize_directories(root)
|
61
52
|
@doc_root_dir = File.join(root, API_DOCS_DIRNAME)
|
62
|
-
|
63
|
-
|
53
|
+
|
64
54
|
# remove previous data (and reset the directory)
|
65
55
|
FileUtils.rm_rf @doc_root_dir if File.exists?(@doc_root_dir)
|
66
56
|
FileUtils.mkdir_p @doc_root_dir unless File.exists? @doc_root_dir
|
@@ -68,7 +58,7 @@ module Praxis
|
|
68
58
|
|
69
59
|
def collect_resources
|
70
60
|
# load all resource definitions registered with Praxis
|
71
|
-
|
61
|
+
Praxis::Application.instance.resource_definitions.map do |resource|
|
72
62
|
# skip resources with doc_visibility of :none
|
73
63
|
next if resource.metadata[:doc_visibility] == :none
|
74
64
|
version = resource.version
|
@@ -86,7 +76,7 @@ module Praxis
|
|
86
76
|
|
87
77
|
def collect_infos
|
88
78
|
# All infos. Including keys for `:global`, "n/a", and any string version
|
89
|
-
@infos_by_version =
|
79
|
+
@infos_by_version = ApiDefinition.instance.describe
|
90
80
|
end
|
91
81
|
|
92
82
|
|
@@ -20,7 +20,7 @@ module Praxis
|
|
20
20
|
|
21
21
|
def endpoint
|
22
22
|
@endpoint ||= begin
|
23
|
-
endpoint =
|
23
|
+
endpoint = ApiDefinition.instance.global_info.documentation_url
|
24
24
|
endpoint.gsub(/\/index\.html$/i, '/') if endpoint
|
25
25
|
end
|
26
26
|
end
|
data/lib/praxis/error_handler.rb
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
module Praxis
|
2
2
|
class ErrorHandler
|
3
|
-
|
4
|
-
def handle!(request, error
|
5
|
-
|
3
|
+
|
4
|
+
def handle!(request, error)
|
5
|
+
Application.instance.logger.error error.inspect
|
6
6
|
error.backtrace.each do |line|
|
7
|
-
|
7
|
+
Application.instance.logger.error line
|
8
8
|
end
|
9
9
|
|
10
10
|
response = Responses::InternalServerError.new(error: error)
|
11
11
|
response.request = request
|
12
|
-
response.finish
|
12
|
+
response.finish
|
13
13
|
end
|
14
14
|
|
15
15
|
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# rubocop:disable all
|
3
|
+
module Praxis
|
4
|
+
module Extensions
|
5
|
+
class SequelFilterQueryBuilder
|
6
|
+
attr_reader :query, :root
|
7
|
+
|
8
|
+
# Abstract class, which needs to be used by subclassing it through the .for method, to set the mapping of attributes
|
9
|
+
class << self
|
10
|
+
def for(definition)
|
11
|
+
Class.new(self) do
|
12
|
+
@attr_to_column = case definition
|
13
|
+
when Hash
|
14
|
+
definition
|
15
|
+
when Array
|
16
|
+
definition.each_with_object({}) { |item, hash| hash[item.to_sym] = item }
|
17
|
+
else
|
18
|
+
raise "Cannot use FilterQueryBuilder.of without passing an array or a hash (Got: #{definition.class.name})"
|
19
|
+
end
|
20
|
+
class << self
|
21
|
+
attr_reader :attr_to_column
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Base query to build upon
|
28
|
+
# table is necessary when use the strin queries, when the query has multiple tables involved
|
29
|
+
# (to disambiguate)
|
30
|
+
def initialize(query:, model: )
|
31
|
+
@query = query
|
32
|
+
@root = model.table_name
|
33
|
+
end
|
34
|
+
|
35
|
+
# By default we'll simply use the incoming op and value, and will map
|
36
|
+
# the attribute based on what's on the `attr_to_column` hash
|
37
|
+
def build_clause(filters)
|
38
|
+
seen_associations = Set.new
|
39
|
+
filters.each do |(attr, spec)|
|
40
|
+
column_name = attr_to_column[attr]
|
41
|
+
raise "Filtering by #{attr} not allowed (no mapping found)" unless column_name
|
42
|
+
if column_name.is_a?(Proc)
|
43
|
+
bindings = column_name.call(spec)
|
44
|
+
# A hash of bindings, consisting of a key with column name and a value to the query value
|
45
|
+
bindings.each{|col,val| expand_binding(column_name: col, op: spec[:op], value: val )}
|
46
|
+
else
|
47
|
+
expand_binding(column_name: column_name, **spec)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
query
|
51
|
+
end
|
52
|
+
|
53
|
+
def expand_binding(column_name:,op:,value:)
|
54
|
+
assoc_or_field, *rest = column_name.to_s.split('.')
|
55
|
+
if rest.empty?
|
56
|
+
column_name = Sequel.qualify(root,column_name)
|
57
|
+
else
|
58
|
+
puts "Adding eager graph for #{assoc_or_field} due to being used in filter"
|
59
|
+
# Ensure the joined table is aliased properly (to the association name) so we can add the condition appropriately
|
60
|
+
@query = query.eager_graph(Sequel.as(assoc_or_field.to_sym, assoc_or_field.to_sym) )
|
61
|
+
column_name = Sequel.qualify(assoc_or_field, rest.first)
|
62
|
+
end
|
63
|
+
add_clause(attr: column_name, op: op, value: value)
|
64
|
+
end
|
65
|
+
|
66
|
+
def attr_to_column
|
67
|
+
# Class method defined by the subclassing Class (using .for)
|
68
|
+
self.class.attr_to_column
|
69
|
+
end
|
70
|
+
|
71
|
+
# Private to try to funnel all column names through `build_clause` that restricts
|
72
|
+
# the attribute names better (to allow more difficult SQL injections )
|
73
|
+
private def add_clause(attr:, op:, value:)
|
74
|
+
# TODO: partial matching
|
75
|
+
#components = attr.to_s.split('.')
|
76
|
+
#attr_selector = Sequel.qualify(*components)
|
77
|
+
attr_selector = attr
|
78
|
+
# HERE!! if we have "association.name" we should properly join it ...!
|
79
|
+
|
80
|
+
#> ds.eager_graph(:device).where{{device[:name] => 'A%'}}.select(:accountID)
|
81
|
+
#=> #<Sequel::Mysql2::Dataset: "SELECT `accountID` FROM `EventData`
|
82
|
+
# LEFT OUTER JOIN `Device` AS `device` ON
|
83
|
+
# ((`device`.`accountID` = `EventData`.`accountID`) AND (`device`.`deviceID` = `EventData`.`deviceID`))
|
84
|
+
# WHERE (`device`.`name` = 'A%')">
|
85
|
+
likeval = get_like_value(value)
|
86
|
+
@query = case op
|
87
|
+
when '='
|
88
|
+
if likeval
|
89
|
+
query.where(Sequel.like(attr_selector, likeval))
|
90
|
+
else
|
91
|
+
query.where(attr_selector => value)
|
92
|
+
end
|
93
|
+
when '!='
|
94
|
+
if likeval
|
95
|
+
query.exclude(Sequel.like(attr_selector, likeval))
|
96
|
+
else
|
97
|
+
query.exclude(attr_selector => value)
|
98
|
+
end
|
99
|
+
when '>'
|
100
|
+
#query.where(Sequel.lit("#{attr_selector} > ?", value))
|
101
|
+
query.where{attr_selector > value}
|
102
|
+
when '<'
|
103
|
+
query.where{attr_selector < value}
|
104
|
+
when '>='
|
105
|
+
query.where{attr_selector >= value}
|
106
|
+
when '<='
|
107
|
+
query.where{attr_selector <= value}
|
108
|
+
else
|
109
|
+
raise "Unsupported Operator!!! #{op}"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# Returns nil if the value was not a fuzzzy pattern
|
114
|
+
def get_like_value(value)
|
115
|
+
if value.is_a?(String) && (value[-1] == '*' || value[0] == '*')
|
116
|
+
likeval = value.dup
|
117
|
+
likeval[-1] = '%' if value[-1] == '*'
|
118
|
+
likeval[0] = '%' if value[0] == '*'
|
119
|
+
likeval
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
# rubocop:enable all
|
@@ -1,13 +1,2 @@
|
|
1
1
|
require 'attributor/extras/field_selector'
|
2
|
-
|
3
|
-
require 'praxis/extensions/field_selection/field_selector'
|
4
|
-
# TODO: we should conditionally require it based on what ORM/s we want...
|
5
|
-
require 'praxis/extensions/field_selection/active_record_query_selector'
|
6
|
-
|
7
|
-
|
8
|
-
module Praxis
|
9
|
-
module Extensions
|
10
|
-
module FieldSelection
|
11
|
-
end
|
12
|
-
end
|
13
|
-
end
|
2
|
+
require 'praxis/extensions/field_selection/field_selector'
|
@@ -3,53 +3,47 @@ module Praxis
|
|
3
3
|
module Extensions
|
4
4
|
module FieldSelection
|
5
5
|
class ActiveRecordQuerySelector
|
6
|
-
attr_reader :selector, :
|
6
|
+
attr_reader :selector, :query
|
7
7
|
# Gets a dataset, a selector...and should return a dataset with the selector definition applied.
|
8
|
-
def initialize(
|
8
|
+
def initialize(query:, selectors:)
|
9
9
|
@selector = selectors
|
10
|
-
@
|
11
|
-
@top_model = model
|
12
|
-
@resolved = resolved
|
13
|
-
@seen = Set.new
|
14
|
-
@root = model.table_name
|
10
|
+
@query = query
|
15
11
|
end
|
16
12
|
|
17
|
-
def
|
18
|
-
if (fields = fields_for(model))
|
19
|
-
# Note, let's always add the pk fields so that associations can load properly
|
20
|
-
fields = fields | [model.primary_key.to_sym]
|
21
|
-
ds.select(*fields)
|
22
|
-
else
|
23
|
-
ds
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
def generate
|
13
|
+
def generate(debug: false)
|
28
14
|
# TODO: unfortunately, I think we can only control the select clauses for the top model
|
29
15
|
# (as I'm not sure ActiveRecord supports expressing it in the join...)
|
30
|
-
@
|
16
|
+
@query = add_select(query: query, selector_node: selector)
|
17
|
+
eager_hash = _eager(selector)
|
31
18
|
|
32
|
-
@
|
19
|
+
@query = @query.includes(eager_hash)
|
20
|
+
explain_query(query, eager_hash) if debug
|
21
|
+
|
22
|
+
@query
|
33
23
|
end
|
34
24
|
|
35
|
-
def
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
@seen << [model, track]
|
42
|
-
assoc_model = model.associations[track][:model]
|
43
|
-
dataset << { track => _eager(assoc_model, resolved[track]) }
|
44
|
-
end
|
25
|
+
def add_select(query:, selector_node:)
|
26
|
+
# We're gonna always require the PK of the model, as it is a special case for AR, and the app itself
|
27
|
+
# might assume it is always there and not be surprised by the fact that if it isn't, it won't blow up
|
28
|
+
# in the same way as any other attribute not being loaded...i.e., ActiveModel::MissingAttributeError: missing attribute: xyz
|
29
|
+
select_fields = selector_node.select + [selector_node.resource.model.primary_key.to_sym]
|
30
|
+
select_fields.empty? ? query : query.select(*select_fields)
|
45
31
|
end
|
46
32
|
|
47
|
-
def
|
48
|
-
|
33
|
+
def _eager(selector_node)
|
34
|
+
selector_node.tracks.each_with_object({}) do |(track_name, track_node), h|
|
35
|
+
h[track_name] = _eager(track_node)
|
36
|
+
end
|
49
37
|
end
|
50
38
|
|
51
|
-
def
|
52
|
-
|
39
|
+
def explain_query(query, eager_hash)
|
40
|
+
prev = ActiveRecord::Base.logger
|
41
|
+
ActiveRecord::Base.logger = Logger.new(STDOUT)
|
42
|
+
ActiveRecord::Base.logger.debug("Query plan for ...#{selector.resource.model} with selectors: #{JSON.generate(selector.dump)}")
|
43
|
+
ActiveRecord::Base.logger.debug(" ActiveRecord query: #{selector.resource.model}.includes(#{eager_hash})")
|
44
|
+
query.explain
|
45
|
+
ActiveRecord::Base.logger.debug("Query plan end")
|
46
|
+
ActiveRecord::Base.logger = prev
|
53
47
|
end
|
54
48
|
end
|
55
49
|
end
|
@@ -1,63 +1,59 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'sequel'
|
4
|
+
|
2
5
|
module Praxis
|
3
6
|
module Extensions
|
4
7
|
module FieldSelection
|
5
8
|
class SequelQuerySelector
|
6
|
-
attr_reader :selector, :
|
9
|
+
attr_reader :selector, :query
|
7
10
|
# Gets a dataset, a selector...and should return a dataset with the selector definition applied.
|
8
|
-
def initialize(
|
11
|
+
def initialize(query:, selectors:)
|
9
12
|
@selector = selectors
|
10
|
-
@
|
11
|
-
@top_model = model
|
12
|
-
@resolved = resolved
|
13
|
-
@seen = Set.new
|
14
|
-
@root = model.table_name
|
13
|
+
@query = query
|
15
14
|
end
|
16
15
|
|
17
|
-
def
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
ds.select(*qualified)
|
23
|
-
else
|
24
|
-
ds
|
16
|
+
def generate(debug: false)
|
17
|
+
@query = add_select(query: query, selector_node: @selector)
|
18
|
+
|
19
|
+
@query = @selector.tracks.inject(@query) do |ds, (track_name, track_node)|
|
20
|
+
ds.eager(track_name => _eager(track_node) )
|
25
21
|
end
|
26
|
-
end
|
27
22
|
|
28
|
-
|
29
|
-
@
|
30
|
-
|
31
|
-
tracks = only_assoc_for(top_model, resolved)
|
32
|
-
@ds = tracks.inject(@ds) do |dataset, track|
|
33
|
-
next dataset if @seen.include?([top_model, track])
|
34
|
-
@seen << [top_model, track]
|
35
|
-
assoc_model = top_model.associations[track][:model]
|
36
|
-
# hash[track] = _eager(assoc_model, resolved[track])
|
37
|
-
dataset.eager(track => _eager(assoc_model, resolved[track]))
|
38
|
-
end
|
23
|
+
explain_query(query) if debug
|
24
|
+
@query
|
39
25
|
end
|
40
26
|
|
41
|
-
def _eager(
|
27
|
+
def _eager(selector_node)
|
42
28
|
lambda do |dset|
|
43
|
-
|
29
|
+
dset = add_select(query: dset, selector_node: selector_node)
|
44
30
|
|
45
|
-
|
46
|
-
|
47
|
-
next dataset if @seen.include?([model, track])
|
48
|
-
@seen << [model, track]
|
49
|
-
assoc_model = model.associations[track][:model]
|
50
|
-
dataset.eager(track => _eager(assoc_model, resolved[track]))
|
31
|
+
dset = selector_node.tracks.inject(dset) do |ds, (track_name, track_node)|
|
32
|
+
ds.eager(track_name => _eager(track_node) )
|
51
33
|
end
|
34
|
+
|
52
35
|
end
|
53
36
|
end
|
54
37
|
|
55
|
-
def
|
56
|
-
|
38
|
+
def add_select(query:, selector_node:)
|
39
|
+
# We're gonna always require the PK of the model, as it is a special case for Sequel, and the app itself
|
40
|
+
# might assume it is always there and not be surprised by the fact that if it isn't, it won't blow up
|
41
|
+
# in the same way as any other attribute not being loaded...i.e., NoMethodError: undefined method `foobar' for #<...>
|
42
|
+
select_fields = selector_node.select + [selector_node.resource.model.primary_key.to_sym]
|
43
|
+
|
44
|
+
table_name = selector_node.resource.model.table_name
|
45
|
+
qualified = select_fields.map { |f| Sequel.qualify(table_name, f) }
|
46
|
+
query.select(*qualified)
|
57
47
|
end
|
58
48
|
|
59
|
-
def
|
60
|
-
|
49
|
+
def explain_query(ds)
|
50
|
+
prev_loggers = Sequel::Model.db.loggers
|
51
|
+
stdout_logger = Logger.new($stdout)
|
52
|
+
Sequel::Model.db.loggers = [stdout_logger]
|
53
|
+
stdout_logger.debug("Query plan for ...#{selector.resource.model} with selectors: #{JSON.generate(selector.dump)}")
|
54
|
+
ds.all
|
55
|
+
stdout_logger.debug("Query plan end")
|
56
|
+
Sequel::Model.db.loggers = prev_loggers
|
61
57
|
end
|
62
58
|
end
|
63
59
|
end
|