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
@@ -17,6 +17,8 @@ module Praxis
17
17
  # @param [String] document
18
18
  # @return [Hash,Array] the structured-data representation of the document
19
19
  def parse(document)
20
+ # Try to be nice and accept an empty string as an empty payload (seems nice to do for dumb http clients)
21
+ return nil if (document.nil? || document == '')
20
22
  ::JSON.parse(document)
21
23
  end
22
24
 
@@ -1,3 +1,8 @@
1
+ # This is an example of a handler that can load and generate www-url-encoded payloads.
2
+ # Note that if you use your API to pass nil values for attributes as a way to unset their
3
+ # values, this handler will not work (as there isn't necessarily a defined "null" value in
4
+ # this encoding (although you can probably define how to encode/decode it and use it as such)
5
+ # Use at your own risk.
1
6
  module Praxis
2
7
  module Handlers
3
8
  class WWWForm
@@ -1,3 +1,9 @@
1
+ # This is an example of a handler that can load and generate 'activesupport-style' xml payloads.
2
+ # Note that if you use your API to pass nil values for attributes as a way to unset their values,
3
+ # this handler will not work (as there isn't necessarily a defined "null" value in this encoding
4
+ # (although you can probably define how to encode/decode it and use it as such)
5
+ # Use at your own risk
6
+
1
7
  module Praxis
2
8
  module Handlers
3
9
  class XML
@@ -54,6 +54,10 @@ module Praxis
54
54
  super(false,**opts) # Links must always describe attributes
55
55
  end
56
56
 
57
+ def self.json_schema_type
58
+ @attribute.type.json_schema_type
59
+ end
60
+
57
61
  def self._finalize!
58
62
  super
59
63
  if @attribute
@@ -3,6 +3,7 @@
3
3
  require 'active_support/concern'
4
4
 
5
5
  require 'praxis/extensions/field_selection/active_record_query_selector'
6
+ require 'praxis/extensions/attribute_filtering/active_record_filter_query_builder'
6
7
 
7
8
  module Praxis
8
9
  module Mapper
@@ -15,7 +16,7 @@ module Praxis
15
16
 
16
17
  module ClassMethods
17
18
  def _filter_query_builder_class
18
- Praxis::Extensions::ActiveRecordFilterQueryBuilder
19
+ Praxis::Extensions::AttributeFiltering::ActiveRecordFilterQueryBuilder
19
20
  end
20
21
 
21
22
  def _field_selector_query_builder_class
@@ -46,18 +47,70 @@ module Praxis
46
47
  else
47
48
  raise "Unknown association type: #{v.class.name} on #{v.klass.name} for #{v.name}"
48
49
  end
50
+ # Call out any local (i.e., of this model) columns that participate in the association
51
+ info[:local_key_columns] = local_columns_used_for_the_association(info[:type], v)
52
+ info[:remote_key_columns] = remote_columns_used_for_the_association(info[:type], v)
49
53
 
50
54
  if v.is_a?(ActiveRecord::Reflection::ThroughReflection)
51
55
  info[:through] = v.through_reflection.name # TODO: is this correct?
52
56
  end
53
-
54
- # TODO: add more keys for the association to make true praxis mapper functions happy
55
57
  hash[k.to_sym] = info
56
58
  end
57
59
  end
58
60
 
61
+ def _join_foreign_key_for(assoc_reflection)
62
+ maj, min, _ = ActiveRecord.gem_version.segments
63
+ if maj >= 6 && min >=1
64
+ assoc_reflection.join_foreign_key.to_sym
65
+ else
66
+ assoc_reflection.join_keys.foreign_key.to_sym
67
+ end
68
+ end
69
+
70
+ def _join_primary_key_for(assoc_reflection)
71
+ maj, min, _ = ActiveRecord.gem_version.segments
72
+ if maj >= 6 && min >=1
73
+ assoc_reflection.join_primary_key.to_sym
74
+ else
75
+ assoc_reflection.join_keys.key.to_sym
76
+ end
77
+ end
78
+ private
79
+ def local_columns_used_for_the_association(type, assoc_reflection)
80
+ case type
81
+ when :one_to_many
82
+ # The associated table will point to us by key (usually the PK, but not always)
83
+ [_join_foreign_key_for(assoc_reflection)]
84
+ when :many_to_one
85
+ # We have the FKs to the associated model
86
+ [_join_foreign_key_for(assoc_reflection)]
87
+ when :many_to_many
88
+ ref = resolve_closest_through_reflection(assoc_reflection)
89
+ # The associated middle table will point to us by key (usually the PK, but not always)
90
+ [_join_foreign_key_for(ref)] # The foreign key that the last through table points to
91
+ else
92
+ raise "association type #{type} not supported"
93
+ end
94
+ end
95
+
96
+ def remote_columns_used_for_the_association(type, assoc_reflection)
97
+ # It seems that since the reflection is the target of the association, using the join_keys.key
98
+ # will always get us the right column
99
+ case type
100
+ when :one_to_many, :many_to_one, :many_to_many
101
+ [_join_primary_key_for(assoc_reflection)]
102
+ else
103
+ raise "association type #{type} not supported"
104
+ end
105
+ end
106
+
107
+ # Keep following the association reflections as long as there are middle ones (i.e., through)
108
+ # until we come to the one next to the source
109
+ def resolve_closest_through_reflection(ref)
110
+ return ref unless ref.through_reflection?
111
+ resolve_closest_through_reflection( ref.through_reflection )
112
+ end
59
113
  end
60
-
61
114
  end
62
115
  end
63
116
  end
@@ -196,26 +196,33 @@ module Praxis::Mapper
196
196
  end
197
197
 
198
198
  # TODO: this shouldn't be needed if we incorporate it with the properties of the mapper...
199
- def self.filters_mapping(hash)
200
- @_filter_query_builder_class = model._filter_query_builder_class.for(**hash)
201
- end
202
-
203
- def self._filter_query_builder_class
204
- @_filter_query_builder_class
199
+ # ...maybe what this means is that we can change it for a better DSL in the resource?
200
+ def self.filters_mapping(definition)
201
+ @_filters_map = \
202
+ case definition
203
+ when Hash
204
+ definition
205
+ when Array
206
+ definition.each_with_object({}) { |item, hash| hash[item.to_sym] = item }
207
+ else
208
+ raise "Resource.filters_mapping only allows a hash or an array"
209
+ end
205
210
  end
206
211
 
207
212
  def self.craft_filter_query(base_query, filters:) # rubocop:disable Metrics/AbcSize
208
- if filters && _filter_query_builder_class
209
- base_query = _filter_query_builder_class.new(query: base_query, model: model).build_clause(filters)
213
+ if filters
214
+ raise "Must define the mapping of filters if want to use Filtering for resource: #{self}" unless @_filters_map
215
+ debug = Praxis::Application.instance.config.mapper.debug_queries
216
+ base_query = model._filter_query_builder_class.new(query: base_query, model: model, filters_map: @_filters_map, debug: debug).generate(filters)
210
217
  end
211
218
 
212
219
  base_query
213
220
  end
214
221
 
215
- def self.craft_field_selection_query(base_query, selectors:, resolved:) # rubocop:disable Metrics/AbcSize
222
+ def self.craft_field_selection_query(base_query, selectors:) # rubocop:disable Metrics/AbcSize
216
223
  if selectors && model._field_selector_query_builder_class
217
- base_query = model._field_selector_query_builder_class.new(query: base_query, model: self.model,
218
- selectors: selectors, resolved: resolved).generate
224
+ debug = Praxis::Application.instance.config.mapper.debug_queries
225
+ base_query = model._field_selector_query_builder_class.new(query: base_query, selectors: selectors, debug: debug).generate
219
226
  end
220
227
 
221
228
  base_query
@@ -1,101 +1,84 @@
1
1
  module Praxis::Mapper
2
- # Generates a set of selectors given a resource and
3
- # list of resource attributes.
4
- class SelectorGenerator
5
- attr_reader :selectors
6
2
 
7
- def initialize
8
- @selectors = Hash.new do |hash, key|
9
- hash[key] = {select: Set.new, track: Set.new}
10
- end
11
- @seen = Hash.new do |hash, resource|
12
- hash[resource] = Set.new
13
- end
14
- end
3
+ class SelectorGeneratorNode
4
+ attr_reader :select, :model, :resource, :tracks
15
5
 
16
- def add(resource, fields)
17
- return if @seen[resource].include? fields
18
- @seen[resource] << fields
6
+ def initialize(resource)
7
+ @resource = resource
19
8
 
20
- fields.each do |name, field|
21
- map_property(resource, name, field)
22
- end
9
+ @select = Set.new
10
+ @select_star = false
11
+ @tracks = Hash.new
23
12
  end
24
13
 
25
- def select_all(resource)
26
- selectors[resource.model][:select] = true
14
+ def add(fields)
15
+ fields.each do |name, field|
16
+ map_property(name, field)
17
+ end
18
+ self
27
19
  end
28
20
 
29
- def map_property(resource, name, fields)
21
+ def map_property(name, fields)
22
+ praxis_compat_model = resource.model && resource.model.respond_to?(:_praxis_associations)
30
23
  if resource.properties.key?(name)
31
- add_property(resource, name, fields)
32
- elsif resource.model._praxis_associations.key?(name)
33
- add_association(resource, name, fields)
24
+ add_property(name, fields)
25
+ elsif praxis_compat_model && resource.model._praxis_associations.key?(name)
26
+ add_association(name, fields)
34
27
  else
35
- add_select(resource, name)
28
+ add_select(name)
36
29
  end
37
30
  end
38
31
 
39
- def add_select(resource, name)
40
- return select_all(resource) if name == :*
41
- return if selectors[resource.model][:select] == true
42
-
43
- selectors[resource.model][:select] << name
44
- end
45
-
46
- def add_track(resource, name)
47
- selectors[resource.model][:track] << name
48
- end
49
-
50
- def add_association(resource, name, fields)
32
+ def add_association(name, fields)
33
+
51
34
  association = resource.model._praxis_associations.fetch(name) do
52
35
  raise "missing association for #{resource} with name #{name}"
53
36
  end
54
37
  associated_resource = resource.model_map[association[:model]]
55
-
56
- case association[:type]
57
- when :many_to_one
58
- add_track(resource, name)
59
- Array(association[:key]).each do |akey|
60
- add_select(resource, akey)
61
- end
62
- when :one_to_many
63
- add_track(resource, name)
64
- Array(association[:key]).each do |akey|
65
- add_select(associated_resource, akey)
66
- end
67
- when :many_to_many
68
- # If we haven't explicitly added the "through" option in the association
69
- # then we'll assume the underlying ORM is able to fill in the gap. We will
70
- # simply add the fields for the associated resource below
71
- if association.key? :through
72
- head, *tail = association[:through]
73
- new_fields = tail.reverse.inject(fields) do |thing, step|
74
- {step => thing}
75
- end
76
- return add_association(resource, head, new_fields)
77
- else
78
- add_track(resource, name)
38
+ unless associated_resource
39
+ raise "Whoops! could not find a resource associated with model #{association[:model]} (root resource #{resource})"
40
+ end
41
+ # Add the required columns in this model to make sure the association can be loaded
42
+ association[:local_key_columns].each {|col| add_select(col) }
43
+
44
+ node = SelectorGeneratorNode.new(associated_resource)
45
+ unless association[:remote_key_columns].empty?
46
+ # Make sure we add the required columns for this association to the remote model query
47
+ fields = {} if fields == true
48
+ new_fields_as_hash = association[:remote_key_columns].each_with_object({}) do|name, hash|
49
+ hash[name] = true
79
50
  end
80
- else
81
- raise "no select applicable for #{association[:type].inspect}"
51
+ fields.merge!(new_fields_as_hash)
82
52
  end
83
53
 
84
- unless fields == true
85
- # recurse into the field
86
- add(associated_resource,fields)
87
- end
54
+ node.add(fields) unless fields == true
55
+
56
+ self.merge_track(name, node)
57
+ end
58
+
59
+ def add_select(name)
60
+ return @select_star = true if name == :*
61
+ return if @select_star
62
+ @select.add name
88
63
  end
89
64
 
90
- def add_property(resource, name, fields)
65
+ def add_property(name, fields)
91
66
  dependencies = resource.properties[name][:dependencies]
67
+ # Always add the underlying association if we're overriding the name...
68
+ praxis_compat_model = resource.model && resource.model.respond_to?(:_praxis_associations)
69
+ if praxis_compat_model && resource.model._praxis_associations.key?(name)
70
+ add_association(name, fields)
71
+ end
92
72
  if dependencies
93
73
  dependencies.each do |dependency|
94
- # if dependency includes the name, then map it directly as the field
74
+ # To detect recursion, let's allow mapping depending fields to the same name of the property
75
+ # but properly detecting if it's a real association...in which case we've already added it above
95
76
  if dependency == name
96
- add_select(resource, name)
77
+ unless praxis_compat_model && resource.model._praxis_associations.key?(name)
78
+ add_select(name)
79
+ end
97
80
  else
98
- apply_dependency(resource, dependency)
81
+ apply_dependency(dependency)
99
82
  end
100
83
  end
101
84
  end
@@ -107,20 +90,61 @@ module Praxis::Mapper
107
90
  {step => thing}
108
91
  end
109
92
 
110
- add_association(resource, head, new_fields)
93
+ add_association(head, new_fields)
111
94
  end
112
95
 
113
- def apply_dependency(resource, dependency)
96
+ def apply_dependency(dependency)
114
97
  case dependency
115
98
  when Symbol
116
- map_property(resource, dependency, {})
99
+ map_property(dependency, true)
117
100
  when String
118
101
  head, tail = dependency.split('.').collect(&:to_sym)
119
102
  raise "String dependencies can not be singular" if tail.nil?
120
103
 
121
- add_association(resource, head, {tail => true})
104
+ add_association(head, {tail => true})
105
+ end
106
+ end
107
+
108
+ def merge_track( track_name, node )
109
+ raise "Cannot merge another node for association #{track_name}: incompatible model" unless node.model == self.model
110
+
111
+ existing = self.tracks[track_name]
112
+ if existing
113
+ node.select.each do|col_name|
114
+ existing.add_select(col_name)
115
+ end
116
+ node.tracks.each do |name, n|
117
+ existing.merge_track(name, n)
118
+ end
119
+ else
120
+ self.tracks[track_name] = node
122
121
  end
123
122
  end
124
123
 
124
+ def dump
125
+ hash = {}
126
+ hash[:model] = resource.model
127
+ if !@select.empty? || @select_star
128
+ hash[:columns] = @select_star ? [ :* ] : @select.to_a
129
+ end
130
+ unless @tracks.empty?
131
+ hash[:tracks] = @tracks.each_with_object({}) {|(name, node), hash| hash[name] = node.dump }
132
+ end
133
+ hash
134
+ end
135
+ end
136
+
137
+ # Generates a set of selectors given a resource and
138
+ # list of resource attributes.
139
+ class SelectorGenerator
140
+ # Entry point
141
+ def add(resource, fields)
142
+ @root = SelectorGeneratorNode.new(resource)
143
+ @root.add(fields)
144
+ end
145
+
146
+ def selectors
147
+ @root
148
+ end
125
149
  end
126
150
  end
@@ -1,23 +1,30 @@
1
1
  require 'active_support/concern'
2
2
 
3
+
3
4
  module Praxis::Mapper
4
5
  module SequelCompat
5
6
  extend ActiveSupport::Concern
6
7
 
7
- `` included do
8
+ included do
8
9
  attr_accessor :_resource
9
10
  end
10
11
 
11
12
  module ClassMethods
12
13
  def _filter_query_builder_class
14
+ # TODO: refactor the query builder, and add the explicit require in this file
13
15
  Praxis::Extensions::SequelFilterQueryBuilder
14
16
  end
15
17
 
18
+ def _field_selector_query_builder_class
19
+ Praxis::Extensions::FieldSelection::SequelQuerySelector
20
+ end
21
+
16
22
  def _praxis_associations
17
23
  orig = self.association_reflections.clone
18
-
19
24
  orig.each do |k,v|
20
25
  v[:model] = v.associated_class
26
+ v[:local_key_columns] = local_columns_used_for_the_association(v[:type], v)
27
+ v[:remote_key_columns] = remote_columns_used_for_the_association(v[:type], v)
21
28
  if v.respond_to?(:primary_key)
22
29
  v[:primary_key] = v.primary_key
23
30
  else
@@ -31,7 +38,40 @@ module Praxis::Mapper
31
38
  orig
32
39
  end
33
40
 
41
+ private
42
+ def local_columns_used_for_the_association(type, assoc_reflection)
43
+ case type
44
+ when :one_to_many
45
+ # The associated table (or middle table if many to many) will point to us by PK
46
+ assoc_reflection[:primary_key_columns]
47
+ when :many_to_one
48
+ # We have the FKs to the associated model
49
+ assoc_reflection[:keys]
50
+ when :many_to_many
51
+ # The middle table if many to many) will point to us by key (usually the PK, but not always)
52
+ assoc_reflection[:left_primary_keys]
53
+ else
54
+ raise "association type #{type} not supported"
55
+ end
56
+ end
57
+
58
+ def remote_columns_used_for_the_association(type, assoc_reflection)
59
+ case type
60
+ when :one_to_many
61
+ # The columns in the associated table that will point back to the original association
62
+ assoc_reflection[:keys]
63
+ when :many_to_one
64
+ # The columns in the associated table that the children will point to (usually the PK, but not always) ??
65
+ [assoc_reflection.associated_class.primary_key]
66
+ when :many_to_many
67
+ # The middle table if many to many will point to us by key (usually the PK, but not always) ??
68
+ [assoc_reflection.associated_class.primary_key]
69
+ else
70
+ raise "association type #{type} not supported"
71
+ end
72
+ end
73
+
34
74
  end
35
75
 
36
76
  end
37
- end
77
+ end