jsonapi-resources 0.7.1.beta2 → 0.8.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: dceb946bf28d998cd1db3e5fbf7b16756cf8de1a
4
- data.tar.gz: 5b5da11044442fc0afd60f0cac50e655692535d9
3
+ metadata.gz: d825825bdf720bb3b498b9975b9455c52df1ed92
4
+ data.tar.gz: fad2b60d570a85cadb6fc44a073cd316d0ed3650
5
5
  SHA512:
6
- metadata.gz: 72b316a3c84777e316599dd3dafc51da115ea7cd7af2ee4cbad8d391fe594543eb81b7efa7834e57f6540f6b7ccfcc137e6d5c38eaa0f59373af6605c3051894
7
- data.tar.gz: f9a961dfe2c6472e37f82c31e6764df600628841b0c05e1f02e3451c2872be068f78fffef1e4ee25406a54d6699b7d2161ccac334de888b56a85ca1b27de56cc
6
+ metadata.gz: 4f10510695282091866599ec8ca994d25ba348e98a12b8cc9501ab9e0a1b6f0f054e43ceb0091078cac26d05b43c6971db22a73350662186b6126d4ce888520d
7
+ data.tar.gz: 9aaa540340d1426f24269dc2bf3813931256b3dfb1e9e0c667967ee103262033ad8834be14a564e1fe817820392e6e80d4001fda3763a8155e0fa4686c4efe74
data/README.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  [![Join the chat at https://gitter.im/cerebris/jsonapi-resources](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/cerebris/jsonapi-resources?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
4
4
 
5
+ **NOTE:** This README is the documentation for `JSONAPI::Resources`. If you are viewing this at the
6
+ [project page on Github](https://github.com/cerebris/jsonapi-resources) you are viewing the documentation for the `master`
7
+ branch. This may contain information that is not relevant to the release you are using. Please see the README for the
8
+ [version](https://github.com/cerebris/jsonapi-resources/releases) you are using.
9
+
10
+ ---
11
+
5
12
  `JSONAPI::Resources`, or "JR", provides a framework for developing a server that complies with the
6
13
  [JSON API](http://jsonapi.org/) specification.
7
14
 
@@ -43,6 +50,7 @@ backed by ActiveRecord models or by custom objects.
43
50
  * [Key Format] (#key-format)
44
51
  * [Routing] (#routing)
45
52
  * [Nested Routes] (#nested-routes)
53
+ * [Authorization](#authorization)
46
54
  * [Configuration] (#configuration)
47
55
  * [Contributing] (#contributing)
48
56
  * [License] (#license)
@@ -91,7 +99,7 @@ class ContactResource < JSONAPI::Resource
91
99
  end
92
100
  ```
93
101
 
94
- A jsonapi-resource generator is avaliable
102
+ A jsonapi-resource generator is available
95
103
  ```
96
104
  rails generate jsonapi:resource contact
97
105
  ```
@@ -333,6 +341,21 @@ The request will look something like:
333
341
  GET /books?include=author&sort=author.name
334
342
  ```
335
343
 
344
+ ###### Default sorting
345
+
346
+ By default JR sorts ascending on the `id` of the primary resource, unless the request specifies an alternate sort order.
347
+ To override this you may override the `self.default_sort` on a `resource`. `default_sort` should return an array of
348
+ `sort_param` hashes. A `sort_param` hash contains a `field` and a `direction`, with `direction` being either `:asc` or
349
+ `:desc`.
350
+
351
+ For example:
352
+
353
+ ```ruby
354
+ def self.default_sort
355
+ [{field: 'name_last', direction: :desc}, {field: 'name_first', direction: :desc}]
356
+ end
357
+ ```
358
+
336
359
  ##### Attribute Formatting
337
360
 
338
361
  Attributes can have a `Format`. By default all attributes use the default formatter. If an attribute has the `format`
@@ -526,10 +549,14 @@ The relationship methods (`relationship`, `has_one`, and `has_many`) support the
526
549
  * `polymorphic` - set to true to identify relationships that are polymorphic.
527
550
  * `relation_name` - the name of the relation to use on the model. A lambda may be provided which allows conditional selection of the relation based on the context.
528
551
  * `always_include_linkage_data` - if set to true, the relationship includes linkage data. Defaults to false if not set.
552
+ * `eager_load_on_include` - if set to false, will not include this relationship in join SQL when requested via an include. You usually want to leave this on, but it will break 'relationships' which are not active record, for example if you want to expose a tree using the `ancestry` gem or similar, or the SQL query becomes too large to handle. Defaults to true if not set.
529
553
 
530
554
  `to_one` relationships support the additional option:
531
555
  * `foreign_key_on` - defaults to `:self`. To indicate that the foreign key is on the related resource specify `:related`.
532
556
 
557
+ `to_many` relationships support the additional option:
558
+ * `reflect` - defaults to `true`. To indicate that updates to the relationship are performed on the related resource, if relationship reflection is turned on. See [Configuration] (#configuration)
559
+
533
560
  Examples:
534
561
 
535
562
  ```ruby
@@ -1118,6 +1145,13 @@ Callbacks can be defined for the following `JSONAPI::Resource` events:
1118
1145
  - `:remove_to_one_link`
1119
1146
  - `:replace_fields`
1120
1147
 
1148
+ ###### Relationship Reflection
1149
+
1150
+ By default updates to relationships only invoke callbacks on the primary
1151
+ Resource. By setting the `use_relationship_reflection` [Configuration] (#configuration) option
1152
+ updates to `has_many` relationships will occur on the related resource, triggering
1153
+ callbacks on both resources.
1154
+
1121
1155
  ##### `JSONAPI::Processor` Callbacks
1122
1156
 
1123
1157
  Callbacks can also be defined for `JSONAPI::Processor` events:
@@ -1174,7 +1208,7 @@ rails generate jsonapi:controller contact
1174
1208
  ###### ResourceControllerMetal
1175
1209
 
1176
1210
  `JSONAPI::Resources` also provides an alternative class to `ResourceController` called `ResourceControllerMetal`.
1177
- In order to provide a lighter weight controller option this strips the controller down to just the classes needed
1211
+ In order to provide a lighter weight controller option this strips the controller down to just the classes needed
1178
1212
  to work with `JSONAPI::Resources`.
1179
1213
 
1180
1214
  For example:
@@ -1186,7 +1220,7 @@ end
1186
1220
  ```
1187
1221
 
1188
1222
  Note: This may not provide all of the expected controller capabilities if you are using additional gems such as DoorKeeper.
1189
-
1223
+
1190
1224
  ###### Serialization Options
1191
1225
 
1192
1226
  Additional options can be passed to the serializer using the `serialization_options` method.
@@ -1222,7 +1256,7 @@ JSONAPI::Resources supports namespacing of controllers and resources. With names
1222
1256
 
1223
1257
  If you namespace your controller it will require a namespaced resource.
1224
1258
 
1225
- In the following example we have a `resource` that isn't namespaced, and one the has now been namespaced. There are
1259
+ In the following example we have a `resource` that isn't namespaced, and one that has now been namespaced. There are
1226
1260
  slight differences between the two resources, as might be seen in a new version of an API:
1227
1261
 
1228
1262
  ```ruby
@@ -1862,6 +1896,15 @@ phone_number_contact GET /phone-numbers/:phone_number_id/contact(.:format) co
1862
1896
 
1863
1897
  ```
1864
1898
 
1899
+ ### Authorization
1900
+
1901
+ Currently `json-api-resources` doesn't come with built-in primitives for authorization. However multiple users of the framework have come up with different approaches, check out:
1902
+
1903
+ - [jsonapi-authorization](https://github.com/venuu/jsonapi-authorization)
1904
+ - [pundit-resources](https://github.com/togglepro/pundit-resources)
1905
+
1906
+ Refer to the comments/discussion [here](https://github.com/cerebris/jsonapi-resources/issues/16#issuecomment-222438975) for the differences between approaches
1907
+
1865
1908
  ## Configuration
1866
1909
 
1867
1910
  JR has a few configuration options. Some have already been mentioned above. To set configuration options create an
@@ -1928,6 +1971,19 @@ JSONAPI.configure do |config|
1928
1971
  # Controls the serialization of resource linkage for non compound documents
1929
1972
  # NOTE: always_include_to_many_linkage_data is not currently implemented
1930
1973
  config.always_include_to_one_linkage_data = false
1974
+
1975
+ # Relationship reflection invokes the related resource when updates
1976
+ # are made to a has_many relationship. By default relationship_reflection
1977
+ # is turned off because it imposes a small performance penalty.
1978
+ config.use_relationship_reflection = false
1979
+
1980
+ # Allows transactions for creating and updating records
1981
+ # Set this to false if your backend does not support transactions (e.g. Mongodb)
1982
+ config.allow_transactions = true
1983
+
1984
+ # Formatter Caching
1985
+ # Set to false to disable caching of string operations on keys and links.
1986
+ config.cache_formatters = true
1931
1987
  end
1932
1988
  ```
1933
1989
 
@@ -21,10 +21,12 @@ module JSONAPI
21
21
  :top_level_meta_record_count_key,
22
22
  :top_level_meta_include_page_count,
23
23
  :top_level_meta_page_count_key,
24
+ :allow_transactions,
24
25
  :exception_class_whitelist,
25
26
  :always_include_to_one_linkage_data,
26
27
  :always_include_to_many_linkage_data,
27
- :cache_formatters
28
+ :cache_formatters,
29
+ :use_relationship_reflection
28
30
 
29
31
  def initialize
30
32
  #:underscored_key, :camelized_key, :dasherized_key, or custom
@@ -80,9 +82,18 @@ module JSONAPI
80
82
  # for a Resource.
81
83
  self.default_processor_klass = JSONAPI::Processor
82
84
 
85
+ # Allows transactions for creating and updating records
86
+ # Set this to false if your backend does not support transactions (e.g. Mongodb)
87
+ self.allow_transactions = true
88
+
83
89
  # Formatter Caching
84
90
  # Set to false to disable caching of string operations on keys and links.
85
91
  self.cache_formatters = true
92
+
93
+ # Relationship reflection invokes the related resource when updates
94
+ # are made to a has_many relationship. By default relationship_reflection
95
+ # is turned off because it imposes a small performance penalty.
96
+ self.use_relationship_reflection = false
86
97
  end
87
98
 
88
99
  def cache_formatters=(bool)
@@ -172,6 +183,8 @@ module JSONAPI
172
183
 
173
184
  attr_writer :top_level_meta_page_count_key
174
185
 
186
+ attr_writer :allow_transactions
187
+
175
188
  attr_writer :exception_class_whitelist
176
189
 
177
190
  attr_writer :always_include_to_one_linkage_data
@@ -179,6 +192,8 @@ module JSONAPI
179
192
  attr_writer :always_include_to_many_linkage_data
180
193
 
181
194
  attr_writer :raise_if_parameters_not_allowed
195
+
196
+ attr_writer :use_relationship_reflection
182
197
  end
183
198
 
184
199
  class << self
@@ -19,7 +19,9 @@ module JSONAPI
19
19
  # }
20
20
  # }
21
21
 
22
- def initialize(includes_array)
22
+ def initialize(resource_klass, includes_array, force_eager_load: false)
23
+ @resource_klass = resource_klass
24
+ @force_eager_load = force_eager_load
23
25
  @include_directives_hash = { include_related: {} }
24
26
  includes_array.each do |include|
25
27
  parse_include(include)
@@ -38,16 +40,27 @@ module JSONAPI
38
40
 
39
41
  def get_related(current_path)
40
42
  current = @include_directives_hash
43
+ current_resource_klass = @resource_klass
41
44
  current_path.split('.').each do |fragment|
42
45
  fragment = fragment.to_sym
43
- current[:include_related][fragment] ||= { include: false, include_related: {} }
46
+
47
+ if current_resource_klass
48
+ current_relationship = current_resource_klass._relationships[fragment]
49
+ current_resource_klass = current_relationship.try(:resource_klass)
50
+ else
51
+ warn "[RELATIONSHIP NOT FOUND] Relationship could not be found for #{current_path}."
52
+ end
53
+
54
+ include_in_join = @force_eager_load || !current_relationship || current_relationship.eager_load_on_include
55
+
56
+ current[:include_related][fragment] ||= { include: false, include_related: {}, include_in_join: include_in_join }
44
57
  current = current[:include_related][fragment]
45
58
  end
46
59
  current
47
60
  end
48
61
 
49
62
  def get_includes(directive)
50
- directive[:include_related].map do |name, directive|
63
+ directive[:include_related].select { |k,v| v[:include_in_join] }.map do |name, directive|
51
64
  sub = get_includes(directive)
52
65
  sub.any? ? { name => sub } : name
53
66
  end
@@ -16,9 +16,10 @@ module JSONAPI
16
16
  # Use transactions if more than one operation and if one of the operations can be transactional
17
17
  # Even if transactional transactions won't be used unless the derived OperationsProcessor supports them.
18
18
  transactional = false
19
+
19
20
  operations.each do |operation|
20
21
  transactional |= operation.transactional?
21
- end
22
+ end if JSONAPI.configuration.allow_transactions
22
23
 
23
24
  transaction(transactional) do
24
25
  # Links and meta data global to the set of operations
@@ -2,7 +2,7 @@ module JSONAPI
2
2
  class Relationship
3
3
  attr_reader :acts_as_set, :foreign_key, :options, :name,
4
4
  :class_name, :polymorphic, :always_include_linkage_data,
5
- :parent_resource
5
+ :parent_resource, :eager_load_on_include
6
6
 
7
7
  def initialize(name, options = {})
8
8
  @name = name.to_s
@@ -13,6 +13,7 @@ module JSONAPI
13
13
  @relation_name = options.fetch(:relation_name, @name)
14
14
  @polymorphic = options.fetch(:polymorphic, false) == true
15
15
  @always_include_linkage_data = options.fetch(:always_include_linkage_data, false) == true
16
+ @eager_load_on_include = options.fetch(:eager_load_on_include, true) == true
16
17
  end
17
18
 
18
19
  alias_method :polymorphic?, :polymorphic
@@ -74,15 +75,19 @@ module JSONAPI
74
75
  end
75
76
 
76
77
  def polymorphic_type
77
- "#{type.to_s.singularize}_type" if polymorphic?
78
+ "#{name}_type" if polymorphic?
78
79
  end
79
80
  end
80
81
 
81
82
  class ToMany < Relationship
83
+ attr_reader :reflect, :inverse_relationship
84
+
82
85
  def initialize(name, options = {})
83
86
  super
84
87
  @class_name = options.fetch(:class_name, name.to_s.camelize.singularize)
85
88
  @foreign_key ||= "#{name.to_s.singularize}_ids".to_sym
89
+ @reflect = options.fetch(:reflect, true) == true
90
+ @inverse_relationship = options.fetch(:inverse_relationship, parent_resource._type.to_s.singularize.to_sym) if parent_resource
86
91
  end
87
92
  end
88
93
  end
@@ -0,0 +1,157 @@
1
+ module JSONAPI
2
+ class RelationshipBuilder
3
+ attr_reader :model_class, :options, :relationship_class
4
+ delegate :register_relationship, to: :@resource_class
5
+
6
+ def initialize(relationship_class, model_class, options)
7
+ @relationship_class = relationship_class
8
+ @model_class = model_class
9
+ @resource_class = options[:parent_resource]
10
+ @options = options
11
+ end
12
+
13
+ def define_relationship_methods(relationship_name)
14
+ # Initialize from an ActiveRecord model's properties
15
+ if model_class && model_class.ancestors.collect{|ancestor| ancestor.name}.include?('ActiveRecord::Base')
16
+ model_association = model_class.reflect_on_association(relationship_name)
17
+ if model_association
18
+ options[:class_name] ||= model_association.class_name
19
+ end
20
+ end
21
+
22
+ relationship = register_relationship(
23
+ relationship_name,
24
+ relationship_class.new(relationship_name, options)
25
+ )
26
+
27
+ foreign_key = define_foreign_key_setter(relationship.foreign_key)
28
+
29
+ case relationship
30
+ when JSONAPI::Relationship::ToOne
31
+ associated = define_resource_relationship_accessor(:one, relationship_name)
32
+ args = [relationship, foreign_key, associated, relationship_name]
33
+
34
+ relationship.belongs_to? ? build_belongs_to(*args) : build_has_one(*args)
35
+ when JSONAPI::Relationship::ToMany
36
+ associated = define_resource_relationship_accessor(:many, relationship_name)
37
+
38
+ build_to_many(relationship, foreign_key, associated, relationship_name)
39
+ end
40
+ end
41
+
42
+ def define_foreign_key_setter(foreign_key)
43
+ define_on_resource "#{foreign_key}=" do |value|
44
+ @model.method("#{foreign_key}=").call(value)
45
+ end
46
+ foreign_key
47
+ end
48
+
49
+ def define_resource_relationship_accessor(type, relationship_name)
50
+ associated_records_method_name = {
51
+ one: "record_for_#{relationship_name}",
52
+ many: "records_for_#{relationship_name}"
53
+ }
54
+ .fetch(type)
55
+
56
+ define_on_resource associated_records_method_name do
57
+ relationship = self.class._relationships[relationship_name]
58
+ relation_name = relationship.relation_name(context: @context)
59
+ records_for(relation_name)
60
+ end
61
+
62
+ associated_records_method_name
63
+ end
64
+
65
+ def build_belongs_to(relationship, foreign_key, associated_records_method_name, relationship_name)
66
+ # Calls method matching foreign key name on model instance
67
+ define_on_resource foreign_key do
68
+ @model.method(foreign_key).call
69
+ end
70
+
71
+ # Returns instantiated related resource object or nil
72
+ define_on_resource relationship_name do |options = {}|
73
+ relationship = self.class._relationships[relationship_name]
74
+
75
+ if relationship.polymorphic?
76
+ associated_model = public_send(associated_records_method_name)
77
+ resource_klass = self.class.resource_for_model(associated_model) if associated_model
78
+ return resource_klass.new(associated_model, @context) if resource_klass
79
+ else
80
+ resource_klass = relationship.resource_klass
81
+ if resource_klass
82
+ associated_model = public_send(associated_records_method_name)
83
+ return associated_model ? resource_klass.new(associated_model, @context) : nil
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ def build_has_one(relationship, foreign_key, associated_records_method_name, relationship_name)
90
+ # Returns primary key name of related resource class
91
+ define_on_resource foreign_key do
92
+ relationship = self.class._relationships[relationship_name]
93
+
94
+ record = public_send(associated_records_method_name)
95
+ return nil if record.nil?
96
+ record.public_send(relationship.resource_klass._primary_key)
97
+ end
98
+
99
+ # Returns instantiated related resource object or nil
100
+ define_on_resource relationship_name do |options = {}|
101
+ relationship = self.class._relationships[relationship_name]
102
+
103
+ resource_klass = relationship.resource_klass
104
+ if resource_klass
105
+ associated_model = public_send(associated_records_method_name)
106
+ return associated_model ? resource_klass.new(associated_model, @context) : nil
107
+ end
108
+ end
109
+ end
110
+
111
+ def build_to_many(relationship, foreign_key, associated_records_method_name, relationship_name)
112
+ # Returns array of primary keys of related resource classes
113
+ define_on_resource foreign_key do
114
+ records = public_send(associated_records_method_name)
115
+ return records.collect do |record|
116
+ record.public_send(relationship.resource_klass._primary_key)
117
+ end
118
+ end
119
+
120
+ # Returns array of instantiated related resource objects
121
+ define_on_resource relationship_name do |options = {}|
122
+ relationship = self.class._relationships[relationship_name]
123
+
124
+ resource_klass = relationship.resource_klass
125
+ records = public_send(associated_records_method_name)
126
+
127
+ filters = options.fetch(:filters, {})
128
+ unless filters.nil? || filters.empty?
129
+ records = resource_klass.apply_filters(records, filters, options)
130
+ end
131
+
132
+ sort_criteria = options.fetch(:sort_criteria, {})
133
+ unless sort_criteria.nil? || sort_criteria.empty?
134
+ order_options = relationship.resource_klass.construct_order_options(sort_criteria)
135
+ records = resource_klass.apply_sort(records, order_options, @context)
136
+ end
137
+
138
+ paginator = options[:paginator]
139
+ if paginator
140
+ records = resource_klass.apply_pagination(records, paginator, order_options)
141
+ end
142
+
143
+ return records.collect do |record|
144
+ if relationship.polymorphic?
145
+ resource_klass = self.class.resource_for_model(record)
146
+ end
147
+ resource_klass.new(record, @context)
148
+ end
149
+ end
150
+ end
151
+
152
+ def define_on_resource(method_name, &block)
153
+ return if @resource_class.method_defined?(method_name)
154
+ @resource_class.inject_method_definition(method_name, block)
155
+ end
156
+ end
157
+ end
@@ -16,7 +16,7 @@ module JSONAPI
16
16
  @operations = []
17
17
  @fields = {}
18
18
  @filters = {}
19
- @sort_criteria = [{ field: 'id', direction: :asc }]
19
+ @sort_criteria = nil
20
20
  @source_klass = nil
21
21
  @source_id = nil
22
22
  @include_directives = nil
@@ -225,7 +225,7 @@ module JSONAPI
225
225
  include.push(unformat_key(included_resource).to_s)
226
226
  end
227
227
 
228
- @include_directives = JSONAPI::IncludeDirectives.new(include)
228
+ @include_directives = JSONAPI::IncludeDirectives.new(@resource_klass, include)
229
229
  end
230
230
 
231
231
  def parse_filters(filters)
@@ -326,7 +326,8 @@ module JSONAPI
326
326
  relationship_type: relationship_type,
327
327
  source_klass: @source_klass,
328
328
  source_id: @source_id,
329
- fields: @fields
329
+ fields: @fields,
330
+ include_directives: @include_directives
330
331
  )
331
332
  end
332
333
 
@@ -340,7 +341,8 @@ module JSONAPI
340
341
  filters: @source_klass.verify_filters(@filters, @context),
341
342
  sort_criteria: @sort_criteria,
342
343
  paginator: @paginator,
343
- fields: @fields
344
+ fields: @fields,
345
+ include_directives: @include_directives
344
346
  )
345
347
  end
346
348
 
@@ -364,7 +366,8 @@ module JSONAPI
364
366
  @resource_klass,
365
367
  context: @context,
366
368
  data: data,
367
- fields: @fields
369
+ fields: @fields,
370
+ include_directives: @include_directives
368
371
  )
369
372
  end
370
373
  rescue JSONAPI::Exceptions::Error => e
@@ -632,7 +635,8 @@ module JSONAPI
632
635
  context: @context,
633
636
  resource_id: key,
634
637
  data: parse_params(data, updatable_fields),
635
- fields: @fields
638
+ fields: @fields,
639
+ include_directives: @include_directives
636
640
  )
637
641
  end
638
642
 
@@ -1,4 +1,5 @@
1
1
  require 'jsonapi/callbacks'
2
+ require 'jsonapi/relationship_builder'
2
3
 
3
4
  module JSONAPI
4
5
  class Resource
@@ -22,6 +23,9 @@ module JSONAPI
22
23
  def initialize(model, context)
23
24
  @model = model
24
25
  @context = context
26
+ @reload_needed = false
27
+ @changing = false
28
+ @save_needed = false
25
29
  end
26
30
 
27
31
  def _model
@@ -63,39 +67,39 @@ module JSONAPI
63
67
  end
64
68
  end
65
69
 
66
- def create_to_many_links(relationship_type, relationship_key_values)
70
+ def create_to_many_links(relationship_type, relationship_key_values, options = {})
67
71
  change :create_to_many_link do
68
- _create_to_many_links(relationship_type, relationship_key_values)
72
+ _create_to_many_links(relationship_type, relationship_key_values, options)
69
73
  end
70
74
  end
71
75
 
72
- def replace_to_many_links(relationship_type, relationship_key_values)
76
+ def replace_to_many_links(relationship_type, relationship_key_values, options = {})
73
77
  change :replace_to_many_links do
74
- _replace_to_many_links(relationship_type, relationship_key_values)
78
+ _replace_to_many_links(relationship_type, relationship_key_values, options)
75
79
  end
76
80
  end
77
81
 
78
- def replace_to_one_link(relationship_type, relationship_key_value)
82
+ def replace_to_one_link(relationship_type, relationship_key_value, options = {})
79
83
  change :replace_to_one_link do
80
- _replace_to_one_link(relationship_type, relationship_key_value)
84
+ _replace_to_one_link(relationship_type, relationship_key_value, options)
81
85
  end
82
86
  end
83
87
 
84
- def replace_polymorphic_to_one_link(relationship_type, relationship_key_value, relationship_key_type)
88
+ def replace_polymorphic_to_one_link(relationship_type, relationship_key_value, relationship_key_type, options = {})
85
89
  change :replace_polymorphic_to_one_link do
86
- _replace_polymorphic_to_one_link(relationship_type, relationship_key_value, relationship_key_type)
90
+ _replace_polymorphic_to_one_link(relationship_type, relationship_key_value, relationship_key_type, options)
87
91
  end
88
92
  end
89
93
 
90
- def remove_to_many_link(relationship_type, key)
94
+ def remove_to_many_link(relationship_type, key, options = {})
91
95
  change :remove_to_many_link do
92
- _remove_to_many_link(relationship_type, key)
96
+ _remove_to_many_link(relationship_type, key, options)
93
97
  end
94
98
  end
95
99
 
96
- def remove_to_one_link(relationship_type)
100
+ def remove_to_one_link(relationship_type, options = {})
97
101
  change :remove_to_one_link do
98
- _remove_to_one_link(relationship_type)
102
+ _remove_to_one_link(relationship_type, options)
99
103
  end
100
104
  end
101
105
 
@@ -189,6 +193,7 @@ module JSONAPI
189
193
 
190
194
  if defined? @model.save
191
195
  saved = @model.save(validate: false)
196
+
192
197
  unless saved
193
198
  if @model.errors.present?
194
199
  fail JSONAPI::Exceptions::ValidationErrors.new(self)
@@ -199,6 +204,8 @@ module JSONAPI
199
204
  else
200
205
  saved = true
201
206
  end
207
+ @model.reload if @reload_needed
208
+ @reload_needed = false
202
209
 
203
210
  @save_needed = !saved
204
211
 
@@ -215,34 +222,87 @@ module JSONAPI
215
222
  fail JSONAPI::Exceptions::RecordLocked.new(e.message)
216
223
  end
217
224
 
218
- def _create_to_many_links(relationship_type, relationship_key_values)
225
+ def reflect_relationship?(relationship, options)
226
+ return false if !relationship.reflect ||
227
+ (!JSONAPI.configuration.use_relationship_reflection || options[:reflected_source])
228
+
229
+ inverse_relationship = relationship.resource_klass._relationships[relationship.inverse_relationship]
230
+ if inverse_relationship.nil?
231
+ warn "Inverse relationship could not be found for #{self.class.name}.#{relationship.name}. Relationship reflection disabled."
232
+ return false
233
+ end
234
+ true
235
+ end
236
+
237
+ def _create_to_many_links(relationship_type, relationship_key_values, options)
219
238
  relationship = self.class._relationships[relationship_type]
220
239
 
221
- relationship_key_values.each do |relationship_key_value|
222
- related_resource = relationship.resource_klass.find_by_key(relationship_key_value, context: @context)
240
+ # check if relationship_key_values are already members of this relationship
241
+ relation_name = relationship.relation_name(context: @context)
242
+ existing_relations = @model.public_send(relation_name).where(relationship.primary_key => relationship_key_values)
243
+ if existing_relations.count > 0
244
+ # todo: obscure id so not to leak info
245
+ fail JSONAPI::Exceptions::HasManyRelationExists.new(existing_relations.first.id)
246
+ end
223
247
 
224
- relation_name = relationship.relation_name(context: @context)
225
- # TODO: Add option to skip relations that already exist instead of returning an error?
226
- relation = @model.public_send(relation_name).where(relationship.primary_key => relationship_key_value).first
227
- if relation.nil?
228
- @model.public_send(relation_name) << related_resource._model
248
+ if options[:reflected_source]
249
+ @model.public_send(relation_name) << options[:reflected_source]._model
250
+ return :completed
251
+ end
252
+
253
+ # load requested related resources
254
+ # make sure they all exist (also based on context) and add them to relationship
255
+
256
+ related_resources = relationship.resource_klass.find_by_keys(relationship_key_values, context: @context)
257
+
258
+ if related_resources.count != relationship_key_values.count
259
+ # todo: obscure id so not to leak info
260
+ fail JSONAPI::Exceptions::RecordNotFound.new('unspecified')
261
+ end
262
+
263
+ reflect = reflect_relationship?(relationship, options)
264
+
265
+ related_resources.each do |related_resource|
266
+ if reflect
267
+ if related_resource.class._relationships[relationship.inverse_relationship].is_a?(JSONAPI::Relationship::ToMany)
268
+ related_resource.create_to_many_links(relationship.inverse_relationship, [id], reflected_source: self)
269
+ else
270
+ related_resource.replace_to_one_link(relationship.inverse_relationship, id, reflected_source: self)
271
+ end
272
+ @reload_needed = true
229
273
  else
230
- fail JSONAPI::Exceptions::HasManyRelationExists.new(relationship_key_value)
274
+ @model.public_send(relation_name) << related_resource._model
231
275
  end
232
276
  end
233
277
 
234
278
  :completed
235
279
  end
236
280
 
237
- def _replace_to_many_links(relationship_type, relationship_key_values)
281
+ def _replace_to_many_links(relationship_type, relationship_key_values, options)
238
282
  relationship = self.class._relationships[relationship_type]
239
- send("#{relationship.foreign_key}=", relationship_key_values)
240
- @save_needed = true
283
+
284
+ reflect = reflect_relationship?(relationship, options)
285
+
286
+ if reflect
287
+ existing = send("#{relationship.foreign_key}")
288
+ to_delete = existing - (relationship_key_values & existing)
289
+ to_delete.each do |key|
290
+ _remove_to_many_link(relationship_type, key, reflected_source: self)
291
+ end
292
+
293
+ to_add = relationship_key_values - (relationship_key_values & existing)
294
+ _create_to_many_links(relationship_type, to_add, {})
295
+
296
+ @reload_needed = true
297
+ else
298
+ send("#{relationship.foreign_key}=", relationship_key_values)
299
+ @save_needed = true
300
+ end
241
301
 
242
302
  :completed
243
303
  end
244
304
 
245
- def _replace_to_one_link(relationship_type, relationship_key_value)
305
+ def _replace_to_one_link(relationship_type, relationship_key_value, options)
246
306
  relationship = self.class._relationships[relationship_type]
247
307
 
248
308
  send("#{relationship.foreign_key}=", relationship_key_value)
@@ -251,7 +311,7 @@ module JSONAPI
251
311
  :completed
252
312
  end
253
313
 
254
- def _replace_polymorphic_to_one_link(relationship_type, key_value, key_type)
314
+ def _replace_polymorphic_to_one_link(relationship_type, key_value, key_type, options)
255
315
  relationship = self.class._relationships[relationship_type.to_sym]
256
316
 
257
317
  _model.public_send("#{relationship.foreign_key}=", key_value)
@@ -262,10 +322,29 @@ module JSONAPI
262
322
  :completed
263
323
  end
264
324
 
265
- def _remove_to_many_link(relationship_type, key)
266
- relation_name = self.class._relationships[relationship_type].relation_name(context: @context)
325
+ def _remove_to_many_link(relationship_type, key, options)
326
+ relationship = self.class._relationships[relationship_type]
327
+
328
+ reflect = reflect_relationship?(relationship, options)
329
+
330
+ if reflect
331
+
332
+ related_resource = relationship.resource_klass.find_by_key(key, context: @context)
333
+
334
+ if related_resource.nil?
335
+ fail JSONAPI::Exceptions::RecordNotFound.new(key)
336
+ else
337
+ if related_resource.class._relationships[relationship.inverse_relationship].is_a?(JSONAPI::Relationship::ToMany)
338
+ related_resource.remove_to_many_link(relationship.inverse_relationship, id, reflected_source: self)
339
+ else
340
+ related_resource.remove_to_one_link(relationship.inverse_relationship, reflected_source: self)
341
+ end
342
+ end
267
343
 
268
- @model.public_send(relation_name).delete(key)
344
+ @reload_needed = true
345
+ else
346
+ @model.public_send(relationship.relation_name(context: @context)).delete(key)
347
+ end
269
348
 
270
349
  :completed
271
350
 
@@ -275,7 +354,7 @@ module JSONAPI
275
354
  fail JSONAPI::Exceptions::RecordNotFound.new(key)
276
355
  end
277
356
 
278
- def _remove_to_one_link(relationship_type)
357
+ def _remove_to_one_link(relationship_type, options)
279
358
  relationship = self.class._relationships[relationship_type]
280
359
 
281
360
  send("#{relationship.foreign_key}=", nil)
@@ -404,6 +483,8 @@ module JSONAPI
404
483
  ActiveSupport::Deprecation.warn('Id without format is no longer supported. Please remove ids from attributes, or specify a format.')
405
484
  end
406
485
 
486
+ check_duplicate_attribute_name(attr) if options[:format].nil?
487
+
407
488
  @_attributes ||= {}
408
489
  @_attributes[attr] = options
409
490
  define_method attr do
@@ -438,6 +519,15 @@ module JSONAPI
438
519
  _add_relationship(Relationship::ToOne, *attrs)
439
520
  end
440
521
 
522
+ def belongs_to(*attrs)
523
+ ActiveSupport::Deprecation.warn "In #{name} you exposed a `has_one` relationship "\
524
+ " using the `belongs_to` class method. We think `has_one`" \
525
+ " is more appropriate. If you know what you're doing," \
526
+ " and don't want to see this warning again, override the" \
527
+ " `belongs_to` class method on your resource."
528
+ _add_relationship(Relationship::ToOne, *attrs)
529
+ end
530
+
441
531
  def has_many(*attrs)
442
532
  _add_relationship(Relationship::ToMany, *attrs)
443
533
  end
@@ -613,7 +703,7 @@ module JSONAPI
613
703
  end
614
704
 
615
705
  if required_includes.any?
616
- records = apply_includes(records, options.merge(include_directives: IncludeDirectives.new(required_includes)))
706
+ records = apply_includes(records, options.merge(include_directives: IncludeDirectives.new(self, required_includes, force_eager_load: true)))
617
707
  end
618
708
 
619
709
  records
@@ -653,14 +743,20 @@ module JSONAPI
653
743
  end
654
744
 
655
745
  def resources_for(records, context)
656
- resources = []
657
- resource_classes = {}
658
- records.each do |model|
659
- resource_class = resource_classes[model.class] ||= self.resource_for_model(model)
660
- resources.push resource_class.new(model, context)
746
+ records.collect do |model|
747
+ resource_class = self.resource_for_model(model)
748
+ resource_class.new(model, context)
661
749
  end
750
+ end
662
751
 
663
- resources
752
+ def find_by_keys(keys, options = {})
753
+ context = options[:context]
754
+ records = records(options)
755
+ records = apply_includes(records, options)
756
+ models = records.where({_primary_key => keys})
757
+ models.collect do |model|
758
+ self.resource_for_model(model).new(model, context)
759
+ end
664
760
  end
665
761
 
666
762
  def find_by_key(key, options = {})
@@ -852,11 +948,17 @@ module JSONAPI
852
948
  end
853
949
  end
854
950
 
951
+ def default_sort
952
+ [{field: 'id', direction: :asc}]
953
+ end
954
+
855
955
  def construct_order_options(sort_params)
956
+ sort_params ||= default_sort
957
+
856
958
  return {} unless sort_params
857
959
 
858
960
  sort_params.each_with_object({}) do |sort, order_hash|
859
- field = sort[:field] == 'id' ? _primary_key : sort[:field]
961
+ field = sort[:field].to_s == 'id' ? _primary_key : sort[:field].to_s
860
962
  order_hash[field] = sort[:direction]
861
963
  end
862
964
  end
@@ -865,117 +967,22 @@ module JSONAPI
865
967
  options = attrs.extract_options!
866
968
  options[:parent_resource] = self
867
969
 
868
- attrs.each do |attr|
869
- relationship_name = attr.to_sym
870
-
970
+ attrs.each do |relationship_name|
871
971
  check_reserved_relationship_name(relationship_name)
972
+ check_duplicate_relationship_name(relationship_name)
872
973
 
873
- # Initialize from an ActiveRecord model's properties
874
- if _model_class && _model_class.ancestors.collect{|ancestor| ancestor.name}.include?('ActiveRecord::Base')
875
- model_association = _model_class.reflect_on_association(relationship_name)
876
- if model_association
877
- options[:class_name] ||= model_association.class_name
878
- end
879
- end
880
-
881
- @_relationships[relationship_name] = relationship = klass.new(relationship_name, options)
882
-
883
- associated_records_method_name = case relationship
884
- when JSONAPI::Relationship::ToOne then "record_for_#{relationship_name}"
885
- when JSONAPI::Relationship::ToMany then "records_for_#{relationship_name}"
886
- end
887
-
888
- foreign_key = relationship.foreign_key
889
-
890
- define_method "#{foreign_key}=" do |value|
891
- @model.method("#{foreign_key}=").call(value)
892
- end unless method_defined?("#{foreign_key}=")
893
-
894
- define_method associated_records_method_name do
895
- relationship = self.class._relationships[relationship_name]
896
- relation_name = relationship.relation_name(context: @context)
897
- records_for(relation_name)
898
- end unless method_defined?(associated_records_method_name)
899
-
900
- if relationship.is_a?(JSONAPI::Relationship::ToOne)
901
- if relationship.belongs_to?
902
- define_method foreign_key do
903
- @model.method(foreign_key).call
904
- end unless method_defined?(foreign_key)
905
-
906
- define_method relationship_name do |options = {}|
907
- relationship = self.class._relationships[relationship_name]
908
-
909
- if relationship.polymorphic?
910
- associated_model = public_send(associated_records_method_name)
911
- resource_klass = self.class.resource_for_model(associated_model) if associated_model
912
- return resource_klass.new(associated_model, @context) if resource_klass
913
- else
914
- resource_klass = relationship.resource_klass
915
- if resource_klass
916
- associated_model = public_send(associated_records_method_name)
917
- return associated_model ? resource_klass.new(associated_model, @context) : nil
918
- end
919
- end
920
- end unless method_defined?(relationship_name)
921
- else
922
- define_method foreign_key do
923
- relationship = self.class._relationships[relationship_name]
924
-
925
- record = public_send(associated_records_method_name)
926
- return nil if record.nil?
927
- record.public_send(relationship.resource_klass._primary_key)
928
- end unless method_defined?(foreign_key)
929
-
930
- define_method relationship_name do |options = {}|
931
- relationship = self.class._relationships[relationship_name]
932
-
933
- resource_klass = relationship.resource_klass
934
- if resource_klass
935
- associated_model = public_send(associated_records_method_name)
936
- return associated_model ? resource_klass.new(associated_model, @context) : nil
937
- end
938
- end unless method_defined?(relationship_name)
939
- end
940
- elsif relationship.is_a?(JSONAPI::Relationship::ToMany)
941
- define_method foreign_key do
942
- records = public_send(associated_records_method_name)
943
- return records.collect do |record|
944
- record.public_send(relationship.resource_klass._primary_key)
945
- end
946
- end unless method_defined?(foreign_key)
947
-
948
- define_method relationship_name do |options = {}|
949
- relationship = self.class._relationships[relationship_name]
950
-
951
- resource_klass = relationship.resource_klass
952
- records = public_send(associated_records_method_name)
953
-
954
- filters = options.fetch(:filters, {})
955
- unless filters.nil? || filters.empty?
956
- records = resource_klass.apply_filters(records, filters, options)
957
- end
958
-
959
- sort_criteria = options.fetch(:sort_criteria, {})
960
- unless sort_criteria.nil? || sort_criteria.empty?
961
- order_options = relationship.resource_klass.construct_order_options(sort_criteria)
962
- records = resource_klass.apply_sort(records, order_options, @context)
963
- end
974
+ JSONAPI::RelationshipBuilder.new(klass, _model_class, options)
975
+ .define_relationship_methods(relationship_name.to_sym)
976
+ end
977
+ end
964
978
 
965
- paginator = options[:paginator]
966
- if paginator
967
- records = resource_klass.apply_pagination(records, paginator, order_options)
968
- end
979
+ # Allows JSONAPI::RelationshipBuilder to access metaprogramming hooks
980
+ def inject_method_definition(name, body)
981
+ define_method(name, body)
982
+ end
969
983
 
970
- return records.collect do |record|
971
- if relationship.polymorphic?
972
- resource_klass = self.class.resource_for_model(record)
973
- end
974
- resource_klass.new(record, @context)
975
- end
976
- end unless method_defined?(relationship_name)
977
- end
978
- end
984
+ def register_relationship(name, relationship_object)
985
+ @_relationships[name] = relationship_object
979
986
  end
980
987
 
981
988
  private
@@ -1000,6 +1007,18 @@ module JSONAPI
1000
1007
  warn "[NAME COLLISION] `#{name}` is a reserved relationship name in #{_resource_name_from_type(_type)}."
1001
1008
  end
1002
1009
  end
1010
+
1011
+ def check_duplicate_relationship_name(name)
1012
+ if _relationships.include?(name.to_sym)
1013
+ warn "[DUPLICATE RELATIONSHIP] `#{name}` has already been defined in #{_resource_name_from_type(_type)}."
1014
+ end
1015
+ end
1016
+
1017
+ def check_duplicate_attribute_name(name)
1018
+ if _attributes.include?(name.to_sym)
1019
+ warn "[DUPLICATE ATTRIBUTE] `#{name}` has already been defined in #{_resource_name_from_type(_type)}."
1020
+ end
1021
+ end
1003
1022
  end
1004
1023
  end
1005
1024
  end
@@ -16,13 +16,14 @@ module JSONAPI
16
16
  # serializer_options: additional options that will be passed to resource meta and links lambdas
17
17
 
18
18
  def initialize(primary_resource_klass, options = {})
19
- @primary_class_name = primary_resource_klass._type
20
- @fields = options.fetch(:fields, {})
21
- @include = options.fetch(:include, [])
22
- @include_directives = options[:include_directives]
23
- @key_formatter = options.fetch(:key_formatter, JSONAPI.configuration.key_formatter)
24
- @id_formatter = ValueFormatter.value_formatter_for(:id)
25
- @link_builder = generate_link_builder(primary_resource_klass, options)
19
+ @primary_resource_klass = primary_resource_klass
20
+ @primary_class_name = primary_resource_klass._type
21
+ @fields = options.fetch(:fields, {})
22
+ @include = options.fetch(:include, [])
23
+ @include_directives = options[:include_directives]
24
+ @key_formatter = options.fetch(:key_formatter, JSONAPI.configuration.key_formatter)
25
+ @id_formatter = ValueFormatter.value_formatter_for(:id)
26
+ @link_builder = generate_link_builder(primary_resource_klass, options)
26
27
  @always_include_to_one_linkage_data = options.fetch(:always_include_to_one_linkage_data,
27
28
  JSONAPI.configuration.always_include_to_one_linkage_data)
28
29
  @always_include_to_many_linkage_data = options.fetch(:always_include_to_many_linkage_data,
@@ -41,7 +42,7 @@ module JSONAPI
41
42
  is_resource_collection = source.respond_to?(:to_ary)
42
43
 
43
44
  @included_objects = {}
44
- @include_directives ||= JSONAPI::IncludeDirectives.new(@include)
45
+ @include_directives ||= JSONAPI::IncludeDirectives.new(@primary_resource_klass, @include)
45
46
 
46
47
  process_primary(source, @include_directives.include_directives)
47
48
 
@@ -219,7 +220,8 @@ module JSONAPI
219
220
  resources.each do |resource|
220
221
  next if self_referential_and_already_in_source(resource)
221
222
  id = resource.id
222
- relationships_only = already_serialized?(relationship.type, id)
223
+ type = resource.class.resource_for_model(resource._model)
224
+ relationships_only = already_serialized?(type, id)
223
225
  if include_linkage && !relationships_only
224
226
  add_included_object(id, object_hash(resource, ia))
225
227
  elsif include_linked_children || relationships_only
@@ -297,9 +299,18 @@ module JSONAPI
297
299
 
298
300
  # Extracts the foreign key value for a to_one relationship.
299
301
  def foreign_key_value(source, relationship)
300
- foreign_key = relationship.foreign_key
301
- value = source.public_send(foreign_key)
302
- @id_formatter.format(value)
302
+ # If you have direct access to the underlying id, you don't have to load the relationship
303
+ # which can save quite a lot of time when loading a lot of data.
304
+ # This does not apply to e.g. has_one :through relationships.
305
+ if source._model.respond_to?("#{relationship.name}_id")
306
+ related_resource_id = source._model.public_send("#{relationship.name}_id")
307
+ return nil unless related_resource_id
308
+ @id_formatter.format(related_resource_id)
309
+ else
310
+ related_resource = source.public_send(relationship.name)
311
+ return nil unless related_resource
312
+ @id_formatter.format(related_resource.id)
313
+ end
303
314
  end
304
315
 
305
316
  def foreign_key_types_and_values(source, relationship)
@@ -317,8 +328,8 @@ module JSONAPI
317
328
  end
318
329
  end
319
330
  else
320
- source.public_send(relationship.foreign_key).map do |value|
321
- [relationship.type, @id_formatter.format(value)]
331
+ source.public_send(relationship.name).map do |value|
332
+ [relationship.type, @id_formatter.format(value.id)]
322
333
  end
323
334
  end
324
335
  end
@@ -1,5 +1,5 @@
1
1
  module JSONAPI
2
2
  module Resources
3
- VERSION = '0.7.1.beta2'
3
+ VERSION = '0.8.0.beta1'
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jsonapi-resources
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.1.beta2
4
+ version: 0.8.0.beta1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dan Gebhardt
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2016-05-31 00:00:00.000000000 Z
12
+ date: 2016-07-22 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
@@ -172,6 +172,7 @@ files:
172
172
  - lib/jsonapi/paginator.rb
173
173
  - lib/jsonapi/processor.rb
174
174
  - lib/jsonapi/relationship.rb
175
+ - lib/jsonapi/relationship_builder.rb
175
176
  - lib/jsonapi/request_parser.rb
176
177
  - lib/jsonapi/resource.rb
177
178
  - lib/jsonapi/resource_controller.rb