praxis 2.0.pre.2 → 2.0.pre.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (102) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +0 -1
  3. data/.ruby-version +1 -0
  4. data/CHANGELOG.md +32 -0
  5. data/Gemfile +1 -1
  6. data/Guardfile +2 -1
  7. data/Rakefile +1 -7
  8. data/TODO.md +28 -0
  9. data/lib/api_browser/package-lock.json +7110 -0
  10. data/lib/praxis.rb +7 -4
  11. data/lib/praxis/action_definition.rb +9 -16
  12. data/lib/praxis/api_general_info.rb +21 -0
  13. data/lib/praxis/application.rb +1 -2
  14. data/lib/praxis/bootloader_stages/routing.rb +2 -4
  15. data/lib/praxis/docs/generator.rb +11 -6
  16. data/lib/praxis/docs/open_api_generator.rb +255 -0
  17. data/lib/praxis/docs/openapi/info_object.rb +31 -0
  18. data/lib/praxis/docs/openapi/media_type_object.rb +59 -0
  19. data/lib/praxis/docs/openapi/operation_object.rb +40 -0
  20. data/lib/praxis/docs/openapi/parameter_object.rb +69 -0
  21. data/lib/praxis/docs/openapi/paths_object.rb +58 -0
  22. data/lib/praxis/docs/openapi/request_body_object.rb +51 -0
  23. data/lib/praxis/docs/openapi/response_object.rb +63 -0
  24. data/lib/praxis/docs/openapi/responses_object.rb +44 -0
  25. data/lib/praxis/docs/openapi/schema_object.rb +87 -0
  26. data/lib/praxis/docs/openapi/server_object.rb +24 -0
  27. data/lib/praxis/docs/openapi/tag_object.rb +21 -0
  28. data/lib/praxis/extensions/attribute_filtering.rb +2 -0
  29. data/lib/praxis/extensions/attribute_filtering/active_record_filter_query_builder.rb +148 -157
  30. data/lib/praxis/extensions/attribute_filtering/active_record_patches.rb +15 -0
  31. data/lib/praxis/extensions/attribute_filtering/active_record_patches/5x.rb +90 -0
  32. data/lib/praxis/extensions/attribute_filtering/active_record_patches/6_0.rb +68 -0
  33. data/lib/praxis/extensions/attribute_filtering/active_record_patches/6_1_plus.rb +58 -0
  34. data/lib/praxis/extensions/attribute_filtering/filter_tree_node.rb +35 -0
  35. data/lib/praxis/extensions/attribute_filtering/filtering_params.rb +13 -12
  36. data/lib/praxis/extensions/attribute_filtering/sequel_filter_query_builder.rb +3 -2
  37. data/lib/praxis/extensions/field_selection/active_record_query_selector.rb +24 -30
  38. data/lib/praxis/extensions/field_selection/field_selector.rb +4 -0
  39. data/lib/praxis/extensions/field_selection/sequel_query_selector.rb +32 -39
  40. data/lib/praxis/extensions/pagination.rb +130 -0
  41. data/lib/praxis/extensions/pagination/active_record_pagination_handler.rb +42 -0
  42. data/lib/praxis/extensions/pagination/header_generator.rb +70 -0
  43. data/lib/praxis/extensions/pagination/ordering_params.rb +234 -0
  44. data/lib/praxis/extensions/pagination/pagination_handler.rb +68 -0
  45. data/lib/praxis/extensions/pagination/pagination_params.rb +374 -0
  46. data/lib/praxis/extensions/pagination/sequel_pagination_handler.rb +45 -0
  47. data/lib/praxis/handlers/json.rb +2 -0
  48. data/lib/praxis/handlers/www_form.rb +5 -0
  49. data/lib/praxis/handlers/{xml.rb → xml-sample.rb} +6 -0
  50. data/lib/praxis/links.rb +4 -0
  51. data/lib/praxis/mapper/active_model_compat.rb +57 -4
  52. data/lib/praxis/mapper/resource.rb +18 -11
  53. data/lib/praxis/mapper/selector_generator.rb +99 -75
  54. data/lib/praxis/mapper/sequel_compat.rb +43 -3
  55. data/lib/praxis/media_type.rb +1 -56
  56. data/lib/praxis/multipart/part.rb +5 -2
  57. data/lib/praxis/plugins/mapper_plugin.rb +17 -3
  58. data/lib/praxis/plugins/pagination_plugin.rb +71 -0
  59. data/lib/praxis/resource_definition.rb +4 -12
  60. data/lib/praxis/response_definition.rb +1 -1
  61. data/lib/praxis/route.rb +2 -4
  62. data/lib/praxis/routing_config.rb +4 -8
  63. data/lib/praxis/tasks/api_docs.rb +23 -0
  64. data/lib/praxis/tasks/routes.rb +10 -15
  65. data/lib/praxis/types/media_type_common.rb +10 -0
  66. data/lib/praxis/types/multipart_array.rb +62 -0
  67. data/lib/praxis/validation_handler.rb +1 -2
  68. data/lib/praxis/version.rb +1 -1
  69. data/praxis.gemspec +7 -5
  70. data/spec/functional_spec.rb +9 -6
  71. data/spec/praxis/action_definition_spec.rb +4 -16
  72. data/spec/praxis/api_general_info_spec.rb +6 -6
  73. data/spec/praxis/extensions/attribute_filtering/active_record_filter_query_builder_spec.rb +304 -0
  74. data/spec/praxis/extensions/attribute_filtering/filter_tree_node_spec.rb +39 -0
  75. data/spec/praxis/extensions/attribute_filtering/filtering_params_spec.rb +34 -0
  76. data/spec/praxis/extensions/field_expansion_spec.rb +6 -24
  77. data/spec/praxis/extensions/field_selection/active_record_query_selector_spec.rb +110 -0
  78. data/spec/praxis/extensions/field_selection/sequel_query_selector_spec.rb +148 -0
  79. data/spec/praxis/extensions/pagination/active_record_pagination_handler_spec.rb +130 -0
  80. data/spec/praxis/extensions/support/spec_resources_active_model.rb +173 -0
  81. data/spec/praxis/extensions/support/spec_resources_sequel.rb +106 -0
  82. data/spec/praxis/mapper/selector_generator_spec.rb +306 -282
  83. data/spec/praxis/media_type_spec.rb +5 -129
  84. data/spec/praxis/request_spec.rb +3 -22
  85. data/spec/praxis/resource_definition_spec.rb +1 -1
  86. data/spec/praxis/response_definition_spec.rb +8 -9
  87. data/spec/praxis/route_spec.rb +2 -9
  88. data/spec/praxis/routing_config_spec.rb +4 -13
  89. data/spec/praxis/types/multipart_array_spec.rb +4 -21
  90. data/spec/spec_app/config/environment.rb +0 -2
  91. data/spec/spec_app/design/api.rb +7 -1
  92. data/spec/spec_app/design/media_types/instance.rb +0 -8
  93. data/spec/spec_app/design/media_types/volume.rb +0 -12
  94. data/spec/spec_app/design/resources/instances.rb +1 -2
  95. data/spec/spec_helper.rb +17 -0
  96. data/spec/support/be_deep_equal_matcher.rb +39 -0
  97. data/spec/support/spec_media_types.rb +0 -73
  98. data/spec/support/spec_resources.rb +42 -49
  99. metadata +75 -40
  100. data/spec/praxis/handlers/xml_spec.rb +0 -177
  101. data/spec/praxis/links_spec.rb +0 -68
  102. data/spec/spec_app/app/models/person.rb +0 -3
@@ -0,0 +1,68 @@
1
+ # FOR AR < 6.1
2
+ module ActiveRecord
3
+ PRAXIS_JOIN_ALIAS_PREFIX = Praxis::Extensions::AttributeFiltering::ALIAS_TABLE_PREFIX
4
+ class Relation
5
+ def construct_join_dependency(associations, join_type) # :nodoc:
6
+ # Praxis: inject references into the join dependency
7
+ ActiveRecord::Associations::JoinDependency.new(
8
+ klass, table, associations, join_type, references: references_values
9
+ )
10
+ end
11
+ end
12
+
13
+ module Associations
14
+ class JoinDependency
15
+ attr_accessor :references
16
+
17
+ private
18
+ def initialize(base, table, associations, join_type, references: nil)
19
+ tree = self.class.make_tree associations
20
+ @references = references # Save the references values into the instance (to use during build)
21
+ built = build(tree, base)
22
+
23
+ @join_root = JoinBase.new(base, table, built)
24
+ @join_type = join_type
25
+ end
26
+
27
+ # Praxis: table aliases for is shared for 5x and 6.0
28
+ def table_aliases_for(parent, node)
29
+ node.reflection.chain.map do |reflection|
30
+ is_root_reflection = reflection == node.reflection
31
+ table = alias_tracker.aliased_table_for(
32
+ reflection.table_name,
33
+ table_alias_for(reflection, parent, !is_root_reflection),
34
+ reflection.klass.type_caster
35
+ )
36
+ # through tables do not need a special alias_path alias (as they shouldn't really referenced by the client)
37
+ if is_root_reflection && node.alias_path
38
+ table = table.left if table.is_a?(Arel::Nodes::TableAlias) #un-alias it if necessary
39
+ table = table.alias(node.alias_path.join('/'))
40
+ end
41
+ table
42
+ end
43
+ end
44
+
45
+ # Praxis: build for is shared for 5x and 6.0
46
+ def build(associations, base_klass, path: [PRAXIS_JOIN_ALIAS_PREFIX])
47
+ associations.map do |name, right|
48
+ reflection = find_reflection base_klass, name
49
+ reflection.check_validity!
50
+ reflection.check_eager_loadable!
51
+
52
+ if reflection.polymorphic?
53
+ raise EagerLoadPolymorphicError.new(reflection)
54
+ end
55
+ # Praxis: set an alias_path in the JoinAssociation if its path matches a requested reference
56
+ child_path = (path && !path.empty?) ? path + [name] : nil
57
+ association = JoinAssociation.new(reflection, build(right, reflection.klass, path: child_path))
58
+ association.alias_path = child_path if references.include?(child_path.join('/'))
59
+ association
60
+ end
61
+ end
62
+
63
+ end
64
+ class ActiveRecord::Associations::JoinDependency::JoinAssociation
65
+ attr_accessor :alias_path
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,58 @@
1
+ # FOR AR >= 6.1
2
+ module ActiveRecord
3
+ PRAXIS_JOIN_ALIAS_PREFIX = Praxis::Extensions::AttributeFiltering::ALIAS_TABLE_PREFIX
4
+ module Associations
5
+ class JoinDependency
6
+
7
+ private
8
+ def make_constraints(parent, child, join_type)
9
+ foreign_table = parent.table
10
+ foreign_klass = parent.base_klass
11
+ child.join_constraints(foreign_table, foreign_klass, join_type, alias_tracker) do |reflection|
12
+ table, terminated = @joined_tables[reflection]
13
+ root = reflection == child.reflection
14
+
15
+ if table && (!root || !terminated)
16
+ @joined_tables[reflection] = [table, root] if root
17
+ next table, true
18
+ end
19
+
20
+ table_name = @references[reflection.name.to_sym]
21
+ # Praxis: set an alias_path in the JoinAssociation if its path matches a requested reference
22
+ table_name = @references[child&.alias_path.join('/').to_sym] unless table_name
23
+
24
+ table = alias_tracker.aliased_table_for(reflection.klass.arel_table, table_name) do
25
+ name = reflection.alias_candidate(parent.table_name)
26
+ root ? name : "#{name}_join"
27
+ end
28
+
29
+ @joined_tables[reflection] ||= [table, root] if join_type == Arel::Nodes::OuterJoin
30
+ table
31
+ end.concat child.children.flat_map { |c| make_constraints(child, c, join_type) }
32
+ end
33
+
34
+ # Praxis: build for is shared for 5x and 6.0
35
+ def build(associations, base_klass, path: [PRAXIS_JOIN_ALIAS_PREFIX])
36
+ associations.map do |name, right|
37
+ reflection = find_reflection base_klass, name
38
+ reflection.check_validity!
39
+ reflection.check_eager_loadable!
40
+
41
+ if reflection.polymorphic?
42
+ raise EagerLoadPolymorphicError.new(reflection)
43
+ end
44
+ # Praxis: set an alias_path in the JoinAssociation if its path matches a requested reference
45
+ child_path = (path && !path.empty?) ? path + [name] : nil
46
+ association = JoinAssociation.new(reflection, build(right, reflection.klass, path: child_path))
47
+ #association.alias_path = child_path if references.include?(child_path.join('/'))
48
+ association.alias_path = child_path # ??? should be the line above no?
49
+ association
50
+ end
51
+ end
52
+ end
53
+
54
+ class ActiveRecord::Associations::JoinDependency::JoinAssociation
55
+ attr_accessor :alias_path
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,35 @@
1
+ module Praxis
2
+ module Extensions
3
+ module AttributeFiltering
4
+ class FilterTreeNode
5
+ attr_reader :path, :conditions, :children
6
+ # # parsed_filters is an Array of {name: X, op: , value: } ... exactly the format of the FilteringParams.load method
7
+ def initialize(parsed_filters, path: [])
8
+ @path = path # Array that marks the tree 'path' to this node (with respect to the absolute root)
9
+ @conditions = [] # Conditions to apply directly to this node
10
+ @children = {} # Hash with a new NodeTree object value, keyed by name
11
+ children_data = {} # Hash with keys as names of the first level component of the children nodes (and values as array of matching filters)
12
+ parsed_filters.map do |hash|
13
+ *components = hash[:name].to_s.split('.')
14
+ if components.empty?
15
+ return
16
+ elsif components.size == 1
17
+ @conditions << hash.slice(:name, :op, :value)
18
+ else
19
+ children_data[components.first] ||= []
20
+ children_data[components.first] << hash
21
+ end
22
+ end
23
+ # An array of FilterTreeNodes corresponding to each children
24
+ @children = children_data.each_with_object({}) do |(name, arr), hash|
25
+ sub_filters = arr.map do |item|
26
+ _parent, *rest = item[:name].to_s.split('.')
27
+ item.merge(name: rest.join('.'))
28
+ end
29
+ hash[name] = self.class.new(sub_filters, path: path + [name] )
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -26,7 +26,6 @@ module Praxis
26
26
  include Attributor::Type
27
27
  include Attributor::Dumpable
28
28
 
29
- # This DSL allows to define which attributes are allowed in the filters, and with which operators
30
29
  class DSLCompiler < Attributor::DSLCompiler
31
30
  # "account.id": { operators: ["=", "!="] },
32
31
  # name: { operators: ["=", "!="], fuzzy_match: true },
@@ -38,8 +37,8 @@ module Praxis
38
37
  end
39
38
 
40
39
  VALUE_REGEX = /[^,&]*/
41
- AVAILABLE_OPERATORS = Set.new(['!=', '>=', '<=', '=', '<', '>']).freeze
42
- FILTER_REGEX = /(?<attribute>([^=!><])+)(?<operator>!=|>=|<=|=|<|>)(?<value>#{VALUE_REGEX}(,#{VALUE_REGEX})*)/
40
+ AVAILABLE_OPERATORS = Set.new(['!=', '>=', '<=', '=', '<', '>','!','!!']).freeze
41
+ FILTER_REGEX = /(?<attribute>([^=!><])+)(?<operator>!=|>=|<=|!!|=|<|>|!)(?<value>#{VALUE_REGEX}(,#{VALUE_REGEX})*)/
43
42
 
44
43
  # Abstract class, which needs to be used by subclassing it through the .for method, to set the allowed filters
45
44
  # definition should be a hash, keyed by field name, which contains a hash that can have two pieces of metadata
@@ -62,6 +61,10 @@ module Praxis
62
61
  end
63
62
  end
64
63
 
64
+ def json_schema_type
65
+ :string
66
+ end
67
+
65
68
  def add_filter(name, operators:, fuzzy:)
66
69
  components = name.to_s.split('.').map(&:to_sym)
67
70
  attribute, enclosing_type = find_filter_attribute(components, media_type)
@@ -176,7 +179,7 @@ module Praxis
176
179
  else
177
180
  value
178
181
  end
179
- arr.push(name: attr_name, specs: { op: match[:operator], value: coerced } )
182
+ arr.push(name: attr_name, op: match[:operator], value: coerced )
180
183
  end
181
184
  new(parsed)
182
185
  end
@@ -204,28 +207,27 @@ module Praxis
204
207
  def validate(_context = Attributor::DEFAULT_ROOT_CONTEXT)
205
208
  parsed_array.each_with_object([]) do |item, errors|
206
209
  attr_name = item[:name]
207
- specs = item[:specs]
208
210
  attr_filters = allowed_filters[attr_name]
209
211
  unless attr_filters
210
212
  errors << "Filtering by #{attr_name} is not allowed. You can filter by #{allowed_filters.keys.map(&:to_s).join(', ')}"
211
213
  next
212
214
  end
213
215
  allowed_operators = attr_filters[:operators]
214
- unless allowed_operators.include?(specs[:op])
215
- errors << "Operator #{specs[:op]} not allowed for filter #{attr_name}"
216
+ unless allowed_operators.include?(item[:op])
217
+ errors << "Operator #{item[:op]} not allowed for filter #{attr_name}"
216
218
  end
217
219
  value_type = attr_filters[:value_type]
218
- value = specs[:value]
220
+ value = item[:value]
219
221
  if value_type && !value_type.valid_type?(value)
220
222
  # Allow a collection of values of the right type for multimatch (if operators are = or !=)
221
- if ['=','!='].include?(specs[:op])
223
+ if ['=','!='].include?(item[:op])
222
224
  coll_type = Attributor::Collection.of(value_type)
223
225
  if !coll_type.valid_type?(value)
224
226
  errors << "Invalid type in filter/s value for #{attr_name} " +\
225
227
  "(one or more of the multiple matches in #{value} are not a #{value_type.name.split('::').last})"
226
228
  end
227
229
  else
228
- errors << "Invalid type in filter value for #{attr_name} (#{value} using '#{specs[:op]}' is not a #{value_type.name.split('::').last})"
230
+ errors << "Invalid type in filter value for #{attr_name} (#{value} using '#{item[:op]}' is not a #{value_type.name.split('::').last})"
229
231
  end
230
232
  end
231
233
 
@@ -243,8 +245,7 @@ module Praxis
243
245
  def dump
244
246
  parsed_array.each_with_object([]) do |item, arr|
245
247
  field = item[:name]
246
- spec = item[:specs]
247
- arr << "#{field}#{spec[:op]}#{spec[:value]}"
248
+ arr << "#{field}#{item[:op]}#{item[:value]}"
248
249
  end.join('&')
249
250
  end
250
251
 
@@ -34,7 +34,8 @@ module Praxis
34
34
 
35
35
  # By default we'll simply use the incoming op and value, and will map
36
36
  # the attribute based on what's on the `attr_to_column` hash
37
- def build_clause(filters)
37
+ def generate(filters)
38
+ raise "Not refactored yet!"
38
39
  seen_associations = Set.new
39
40
  filters.each do |(attr, spec)|
40
41
  column_name = attr_to_column[attr]
@@ -68,7 +69,7 @@ module Praxis
68
69
  self.class.attr_to_column
69
70
  end
70
71
 
71
- # Private to try to funnel all column names through `build_clause` that restricts
72
+ # Private to try to funnel all column names through `generate` that restricts
72
73
  # the attribute names better (to allow more difficult SQL injections )
73
74
  private def add_clause(attr:, op:, value:)
74
75
  # TODO: partial matching
@@ -3,51 +3,45 @@ module Praxis
3
3
  module Extensions
4
4
  module FieldSelection
5
5
  class ActiveRecordQuerySelector
6
- attr_reader :selector, :query, :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(query:, model:, selectors:, resolved:)
8
+ def initialize(query:, selectors:, debug: false)
9
9
  @selector = selectors
10
10
  @query = query
11
- @top_model = model
12
- @resolved = resolved
13
- @seen = Set.new
14
- @root = model.table_name
15
- end
16
-
17
- def add_select(query:, 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
- query.select(*fields)
22
- else
23
- query
24
- end
11
+ @logger = debug ? Logger.new(STDOUT) : nil
25
12
  end
26
13
 
27
14
  def generate
28
15
  # TODO: unfortunately, I think we can only control the select clauses for the top model
29
16
  # (as I'm not sure ActiveRecord supports expressing it in the join...)
30
- @query = add_select(query: query, model: top_model, table_name: root)
17
+ @query = add_select(query: query, selector_node: selector)
18
+ eager_hash = _eager(selector)
31
19
 
32
- @query.includes(_eager(top_model, resolved) )
20
+ @query = @query.includes(eager_hash)
21
+ explain_query(query, eager_hash) if @logger
22
+
23
+ @query
33
24
  end
34
25
 
35
- def _eager(model, resolved)
36
- tracks = only_assoc_for(model, resolved)
37
- tracks.inject([]) do |dataset, track|
38
- next dataset if @seen.include?([model, track])
39
- @seen << [model, track]
40
- assoc_model = model._praxis_associations[track][:model]
41
- dataset << { track => _eager(assoc_model, resolved[track]) }
42
- end
26
+ 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
28
+ # might assume it is always there and not be surprised by the fact that if it isn't, it won't blow up
29
+ # in the same way as any other attribute not being loaded...i.e., ActiveModel::MissingAttributeError: missing attribute: xyz
30
+ select_fields = selector_node.select + [selector_node.resource.model.primary_key.to_sym]
31
+ select_fields.empty? ? query : query.select(*select_fields)
43
32
  end
44
33
 
45
- def only_assoc_for(model, hash)
46
- hash.keys.reject { |assoc| model._praxis_associations[assoc].nil? }
34
+ def _eager(selector_node)
35
+ selector_node.tracks.each_with_object({}) do |(track_name, track_node), h|
36
+ h[track_name] = _eager(track_node)
37
+ end
47
38
  end
48
39
 
49
- def fields_for(model)
50
- selector[model][:select].to_a
40
+ def explain_query(query, eager_hash)
41
+ @logger.debug("Query plan for ...#{selector.resource.model} with selectors: #{JSON.generate(selector.dump)}")
42
+ @logger.debug(" ActiveRecord query: #{selector.resource.model}.includes(#{eager_hash})")
43
+ query.explain
44
+ @logger.debug("Query plan end")
51
45
  end
52
46
  end
53
47
  end
@@ -7,6 +7,10 @@ module Praxis
7
7
  include Attributor::Type
8
8
  include Attributor::Dumpable
9
9
 
10
+ def self.json_schema_type
11
+ :string
12
+ end
13
+
10
14
  def self.native_type
11
15
  self
12
16
  end
@@ -1,63 +1,56 @@
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(query:, model:, selectors:, resolved:)
11
+ def initialize(query:, selectors:, debug: false)
9
12
  @selector = selectors
10
- @ds = query
11
- @top_model = model
12
- @resolved = resolved
13
- @seen = Set.new
14
- @root = model.table_name
15
- end
16
-
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
25
- end
13
+ @query = query
14
+ @logger = debug ? Logger.new(STDOUT) : nil
26
15
  end
27
16
 
28
17
  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._praxis_associations[track][:model]
36
- # hash[track] = _eager(assoc_model, resolved[track])
37
- dataset.eager(track => _eager(assoc_model, resolved[track]))
18
+ @query = add_select(query: query, selector_node: @selector)
19
+
20
+ @query = @selector.tracks.inject(@query) do |ds, (track_name, track_node)|
21
+ ds.eager(track_name => _eager(track_node) )
38
22
  end
23
+
24
+ explain_query(query) if @logger
25
+ @query
39
26
  end
40
27
 
41
- def _eager(model, resolved)
28
+ def _eager(selector_node)
42
29
  lambda do |dset|
43
- d = add_select(ds: dset, model: model, table_name: model.table_name)
30
+ dset = add_select(query: dset, selector_node: selector_node)
44
31
 
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._praxis_associations[track][:model]
50
- dataset.eager(track => _eager(assoc_model, resolved[track]))
32
+ dset = selector_node.tracks.inject(dset) do |ds, (track_name, track_node)|
33
+ ds.eager(track_name => _eager(track_node) )
51
34
  end
35
+
52
36
  end
53
37
  end
54
38
 
55
- def only_assoc_for(model, hash)
56
- hash.keys.reject { |assoc| model._praxis_associations[assoc].nil? }
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
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
+ # in the same way as any other attribute not being loaded...i.e., NoMethodError: undefined method `foobar' for #<...>
43
+ select_fields = selector_node.select + [selector_node.resource.model.primary_key.to_sym]
44
+
45
+ table_name = selector_node.resource.model.table_name
46
+ qualified = select_fields.map { |f| Sequel.qualify(table_name, f) }
47
+ query.select(*qualified)
57
48
  end
58
49
 
59
- def fields_for(model)
60
- selector[model][:select].to_a
50
+ def explain_query(ds)
51
+ @logger.debug("Query plan for ...#{selector.resource.model} with selectors: #{JSON.generate(selector.dump)}")
52
+ ds.all
53
+ @logger.debug("Query plan end")
61
54
  end
62
55
  end
63
56
  end