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.
Files changed (96) hide show
  1. checksums.yaml +5 -5
  2. data/.travis.yml +5 -20
  3. data/CHANGELOG.md +328 -323
  4. data/lib/praxis.rb +13 -9
  5. data/lib/praxis/action_definition.rb +8 -10
  6. data/lib/praxis/action_definition/headers_dsl_compiler.rb +1 -1
  7. data/lib/praxis/api_definition.rb +27 -44
  8. data/lib/praxis/api_general_info.rb +2 -3
  9. data/lib/praxis/application.rb +15 -142
  10. data/lib/praxis/bootloader.rb +1 -2
  11. data/lib/praxis/bootloader_stages/environment.rb +13 -0
  12. data/lib/praxis/config.rb +1 -1
  13. data/lib/praxis/controller.rb +0 -2
  14. data/lib/praxis/dispatcher.rb +4 -6
  15. data/lib/praxis/docs/generator.rb +8 -18
  16. data/lib/praxis/docs/link_builder.rb +1 -1
  17. data/lib/praxis/error_handler.rb +5 -5
  18. data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +1 -1
  19. data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +1 -1
  20. data/lib/praxis/extensions/attribute_filtering/sequel_filter_query_builder.rb +125 -0
  21. data/lib/praxis/extensions/field_selection.rb +1 -12
  22. data/lib/praxis/extensions/field_selection/active_record_query_selector.rb +28 -34
  23. data/lib/praxis/extensions/field_selection/sequel_query_selector.rb +35 -39
  24. data/lib/praxis/extensions/rendering.rb +1 -1
  25. data/lib/praxis/file_group.rb +1 -1
  26. data/lib/praxis/handlers/xml.rb +1 -1
  27. data/lib/praxis/mapper/active_model_compat.rb +98 -0
  28. data/lib/praxis/mapper/resource.rb +242 -0
  29. data/lib/praxis/mapper/selector_generator.rb +154 -0
  30. data/lib/praxis/mapper/sequel_compat.rb +76 -0
  31. data/lib/praxis/media_type_identifier.rb +2 -1
  32. data/lib/praxis/middleware_app.rb +13 -15
  33. data/lib/praxis/multipart/part.rb +3 -5
  34. data/lib/praxis/notifications.rb +1 -1
  35. data/lib/praxis/plugins/mapper_plugin.rb +64 -0
  36. data/lib/praxis/request.rb +14 -7
  37. data/lib/praxis/request_stages/response.rb +2 -3
  38. data/lib/praxis/resource_definition.rb +15 -19
  39. data/lib/praxis/response.rb +6 -5
  40. data/lib/praxis/response_definition.rb +5 -7
  41. data/lib/praxis/response_template.rb +3 -4
  42. data/lib/praxis/responses/http.rb +36 -0
  43. data/lib/praxis/responses/internal_server_error.rb +12 -3
  44. data/lib/praxis/responses/multipart_ok.rb +11 -4
  45. data/lib/praxis/responses/validation_error.rb +10 -1
  46. data/lib/praxis/route.rb +1 -1
  47. data/lib/praxis/router.rb +3 -3
  48. data/lib/praxis/routing_config.rb +1 -1
  49. data/lib/praxis/tasks/api_docs.rb +2 -10
  50. data/lib/praxis/tasks/routes.rb +0 -1
  51. data/lib/praxis/trait.rb +1 -1
  52. data/lib/praxis/types/media_type_common.rb +2 -2
  53. data/lib/praxis/types/multipart.rb +1 -1
  54. data/lib/praxis/types/multipart_array.rb +2 -2
  55. data/lib/praxis/types/multipart_array/part_definition.rb +1 -1
  56. data/lib/praxis/version.rb +1 -1
  57. data/praxis.gemspec +11 -9
  58. data/spec/functional_spec.rb +0 -1
  59. data/spec/praxis/action_definition_spec.rb +16 -27
  60. data/spec/praxis/api_definition_spec.rb +8 -13
  61. data/spec/praxis/api_general_info_spec.rb +8 -3
  62. data/spec/praxis/application_spec.rb +8 -14
  63. data/spec/praxis/collection_spec.rb +3 -2
  64. data/spec/praxis/config_spec.rb +2 -2
  65. data/spec/praxis/extensions/field_selection/active_record_query_selector_spec.rb +106 -0
  66. data/spec/praxis/extensions/field_selection/sequel_query_selector_spec.rb +147 -0
  67. data/spec/praxis/extensions/field_selection/support/spec_resources_active_model.rb +130 -0
  68. data/spec/praxis/extensions/field_selection/support/spec_resources_sequel.rb +106 -0
  69. data/spec/praxis/handlers/xml_spec.rb +2 -2
  70. data/spec/praxis/mapper/resource_spec.rb +169 -0
  71. data/spec/praxis/mapper/selector_generator_spec.rb +325 -0
  72. data/spec/praxis/media_type_spec.rb +0 -10
  73. data/spec/praxis/middleware_app_spec.rb +16 -10
  74. data/spec/praxis/request_spec.rb +7 -17
  75. data/spec/praxis/request_stages/action_spec.rb +8 -1
  76. data/spec/praxis/request_stages/validate_spec.rb +1 -1
  77. data/spec/praxis/resource_definition_spec.rb +10 -12
  78. data/spec/praxis/response_definition_spec.rb +12 -26
  79. data/spec/praxis/response_spec.rb +6 -13
  80. data/spec/praxis/responses/internal_server_error_spec.rb +5 -2
  81. data/spec/praxis/router_spec.rb +5 -9
  82. data/spec/spec_app/app/controllers/instances.rb +1 -1
  83. data/spec/spec_app/config.ru +6 -1
  84. data/spec/spec_app/config/environment.rb +3 -21
  85. data/spec/spec_helper.rb +13 -17
  86. data/spec/support/be_deep_equal_matcher.rb +39 -0
  87. data/spec/support/spec_resources.rb +124 -0
  88. metadata +74 -53
  89. data/lib/praxis/extensions/attribute_filtering.rb +0 -28
  90. data/lib/praxis/extensions/attribute_filtering/query_builder.rb +0 -39
  91. data/lib/praxis/extensions/mapper_selectors.rb +0 -16
  92. data/lib/praxis/media_type_collection.rb +0 -127
  93. data/lib/praxis/plugins/praxis_mapper_plugin.rb +0 -246
  94. data/spec/praxis/media_type_collection_spec.rb +0 -157
  95. data/spec/praxis/plugins/praxis_mapper_plugin_spec.rb +0 -142
  96. data/spec/spec_app/app/models/person.rb +0 -3
@@ -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!(application: self.application)
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
 
@@ -54,7 +54,7 @@ module Praxis
54
54
  )
55
55
  end
56
56
  top.options.merge!(opts)
57
- top.type.attributes(opts, &block)
57
+ top.type.attributes(**opts, &block)
58
58
  else
59
59
  @attribute.attributes[key] = Attributor::Attribute.new(type, opts, &block)
60
60
  end
@@ -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
 
@@ -28,13 +28,11 @@ module Praxis
28
28
  @deferred_callbacks[:after] << [conditions, block]
29
29
  end
30
30
 
31
- # Typically, this is only called from the router, and the app will always be known.
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(application: application)
107
+ controller.response.finish
110
108
  rescue => e
111
- @application.error_handler.handle!(request, e, app: application)
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
- def self.generate(root, name:, skip_sub_directory: false)
29
- instance = Praxis::Application.registered_apps[name]
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
- @app_instance = instance
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, subdir: nil )
51
+ def initialize_directories(root)
61
52
  @doc_root_dir = File.join(root, API_DOCS_DIRNAME)
62
- @doc_root_dir = File.join(@doc_root_dir, subdir) if subdir
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
- app_instance.resource_definitions.map do |resource|
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 = app_instance.api_definition.describe
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 = Application.current_instance.api_definition.global_info.documentation_url
23
+ endpoint = ApiDefinition.instance.global_info.documentation_url
24
24
  endpoint.gsub(/\/index\.html$/i, '/') if endpoint
25
25
  end
26
26
  end
@@ -1,15 +1,15 @@
1
1
  module Praxis
2
2
  class ErrorHandler
3
-
4
- def handle!(request, error, app:)
5
- app.logger.error error.inspect
3
+
4
+ def handle!(request, error)
5
+ Application.instance.logger.error error.inspect
6
6
  error.backtrace.each do |line|
7
- app.logger.error line
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(application: app)
12
+ response.finish
13
13
  end
14
14
 
15
15
  end
@@ -23,7 +23,7 @@ module Praxis
23
23
  end
24
24
 
25
25
  # Base query to build upon
26
- def initialize(query: , model: )
26
+ def initialize(query: , model:)
27
27
  @query = query
28
28
  @table = model.table_name
29
29
  @last_join_alias = model.table_name
@@ -159,7 +159,7 @@ module Praxis
159
159
  values
160
160
  else
161
161
  multimatch = false
162
- match[:value]
162
+ values.first
163
163
  end
164
164
 
165
165
  attr_name = match[:attribute].to_sym
@@ -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, :ds, :top_model, :resolved, :root
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(ds:, model:, selectors:, resolved:)
8
+ def initialize(query:, selectors:)
9
9
  @selector = selectors
10
- @ds = ds
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 add_select(ds:, model:, table_name:)
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
- @ds = add_select(ds: ds, model: top_model, table_name: root)
16
+ @query = add_select(query: query, selector_node: selector)
17
+ eager_hash = _eager(selector)
31
18
 
32
- @ds.includes(_eager(top_model, resolved) )
19
+ @query = @query.includes(eager_hash)
20
+ explain_query(query, eager_hash) if debug
21
+
22
+ @query
33
23
  end
34
24
 
35
- def _eager(model, resolved)
36
- # Cannot select fields in included rels...boooo :()
37
- # d = add_select(ds: dset, model: model, table_name: model.table_name)
38
- tracks = only_assoc_for(model, resolved)
39
- tracks.inject([]) do |dataset, track|
40
- next dataset if @seen.include?([model, track])
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 only_assoc_for(model, hash)
48
- hash.keys.reject { |assoc| model.associations[assoc].nil? }
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 fields_for(model)
52
- selector[model][:select].to_a
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, :ds, :top_model, :resolved, :root
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(ds:, model:, selectors:, resolved:)
11
+ def initialize(query:, selectors:)
9
12
  @selector = selectors
10
- @ds = ds
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 add_select(ds:, model:, table_name:)
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 | [:id]
21
- qualified = fields.map { |f| Sequel.qualify(table_name, f) }
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
- def generate
29
- @ds = add_select(ds: ds, model: top_model, table_name: root)
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(model, resolved)
27
+ def _eager(selector_node)
42
28
  lambda do |dset|
43
- d = add_select(ds: dset, model: model, table_name: model.table_name)
29
+ dset = add_select(query: dset, selector_node: selector_node)
44
30
 
45
- tracks = only_assoc_for(model, resolved)
46
- tracks.inject(d) do |dataset, track|
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 only_assoc_for(model, hash)
56
- hash.keys.reject { |assoc| model.associations[assoc].nil? }
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 fields_for(model)
60
- selector[model][:select].to_a
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