parse-stack 1.5.1 → 1.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +15 -1
  3. data/Gemfile.lock +10 -10
  4. data/README.md +23 -9
  5. data/bin/console +3 -0
  6. data/lib/parse/api/analytics.rb +1 -1
  7. data/lib/parse/api/objects.rb +1 -1
  8. data/lib/parse/api/users.rb +1 -1
  9. data/lib/parse/client.rb +77 -40
  10. data/lib/parse/client/caching.rb +9 -5
  11. data/lib/parse/client/protocol.rb +47 -0
  12. data/lib/parse/client/request.rb +66 -37
  13. data/lib/parse/client/response.rb +39 -21
  14. data/lib/parse/model/acl.rb +4 -9
  15. data/lib/parse/model/associations/belongs_to.rb +97 -9
  16. data/lib/parse/model/associations/collection_proxy.rb +89 -29
  17. data/lib/parse/model/associations/has_many.rb +301 -28
  18. data/lib/parse/model/associations/has_one.rb +98 -4
  19. data/lib/parse/model/associations/pointer_collection_proxy.rb +48 -16
  20. data/lib/parse/model/associations/relation_collection_proxy.rb +61 -36
  21. data/lib/parse/model/bytes.rb +11 -5
  22. data/lib/parse/model/classes/installation.rb +50 -3
  23. data/lib/parse/model/classes/role.rb +7 -2
  24. data/lib/parse/model/classes/session.rb +21 -4
  25. data/lib/parse/model/classes/user.rb +122 -22
  26. data/lib/parse/model/core/actions.rb +7 -3
  27. data/lib/parse/model/core/properties.rb +14 -13
  28. data/lib/parse/model/core/querying.rb +16 -10
  29. data/lib/parse/model/core/schema.rb +2 -3
  30. data/lib/parse/model/date.rb +18 -12
  31. data/lib/parse/model/file.rb +77 -19
  32. data/lib/parse/model/geopoint.rb +70 -12
  33. data/lib/parse/model/model.rb +84 -8
  34. data/lib/parse/model/object.rb +225 -94
  35. data/lib/parse/model/pointer.rb +94 -13
  36. data/lib/parse/model/push.rb +76 -4
  37. data/lib/parse/query.rb +356 -41
  38. data/lib/parse/query/constraints.rb +399 -29
  39. data/lib/parse/query/ordering.rb +21 -8
  40. data/lib/parse/stack.rb +1 -0
  41. data/lib/parse/stack/version.rb +2 -1
  42. data/lib/parse/webhooks.rb +0 -24
  43. data/lib/parse/webhooks/payload.rb +54 -1
  44. data/lib/parse/webhooks/registration.rb +13 -2
  45. metadata +2 -2
@@ -7,34 +7,56 @@ require 'active_support/inflector'
7
7
  require 'active_support/core_ext/object'
8
8
  require_relative '../pointer'
9
9
 
10
- # A collection proxy is a special type of array wrapper that will allow us to
11
- # notify the parent object about changes to the array. We use a delegate pattern
12
- # to send notifications to the parent whenever the content of the internal array changes.
13
- # The main requirement to using the proxy is to provide the list of initial items if any,
14
- # the owner to be notified and the name of the attribute 'key'. With that, anytime the array
15
- # will change, we will notify the delegate by sending :'key'_will_change! . The proxy can also
16
- # be lazy when fetching the contents of the collection. Whenever the collection is accessed and
17
- # the list is in a "not loaded" state (empty and loaded == false), we will send :'key_fetch!' to the delegate in order to
18
- # populate the collection.
19
- module Parse
20
10
 
11
+ module Parse
12
+ # We use a delegate pattern to send notifications to the parent whenever the content of the internal array changes.
13
+ # The main requirement to using the proxy is to provide the list of initial items if any,
14
+ # the owner to be notified and the name of the attribute 'key'. With that, anytime the array
15
+ # will change, we will notify the delegate by sending :'key'_will_change! . The proxy can also
16
+ # be lazy when fetching the contents of the collection. Whenever the collection is accessed and
17
+ # the list is in a "not loaded" state (empty and loaded == false), we will send :'key_fetch!' to the delegate in order to
18
+ # populate the collection.
19
+
20
+ # A CollectionProxy is a special type of array wrapper that notifies a delegate
21
+ # object about changes to the array in order to perform dirty tracking. This is
22
+ # used for all Array properties in Parse::Objects.
21
23
  class CollectionProxy
22
24
  include ::ActiveModel::Model
23
25
  include ::ActiveModel::Dirty
24
26
  include ::Enumerable
25
- attr_accessor :collection, :delegate, :loaded
27
+ # @!attribute [rw] collection
28
+ # The internal backing store of the collection.
29
+ # @return [Array]
30
+
31
+ # @!attribute [r] delegate
32
+ # The object to be notified of changes to the collection.
33
+ # @return [Object]
34
+
35
+ # @!attribute [rw] loaded
36
+ # @return [Boolean] true/false whether the collection has been loaded.
37
+
38
+ # @!attribute [r] parse_class
39
+ # For some subclasses, this helps typecast the items in the collection.
40
+ # @return [String]
41
+
42
+ # @!attribute [r] key
43
+ # the name of the property key to use when sending notifications for _will_change! and _fetch!
44
+ # @return [String]
45
+
46
+ attr_accessor :collection, :delegate, :loaded, :parse_class
26
47
  attr_reader :delegate, :key
27
- attr_accessor :parse_class
48
+
28
49
  # This is to use dirty tracking within the proxy
29
50
  define_attribute_methods :collection
30
- include Enumerable
31
-
32
- # To initialize a collection, you need to pass the following named parameters
33
- # collection - the initial items to add to the collection.
34
- # :delegate - the owner of the object that will receive the notifications.
35
- # :key - the name of the key to use when sending notifications for _will_change! and _fetch!
36
- # :parse_class - what Parse class type are the items of the collection.
37
- # This is used to typecast the objects in the array to a particular Parse Object type.
51
+
52
+ # Create a new CollectionProxy instance.
53
+ # @param collection [Array] the initial items to add to the collection.
54
+ # @param delegate [Object] the owner of the object that will receive the notifications.
55
+ # @param key [Symbol] the name of the key to use when sending notifications for _will_change! and _fetch!
56
+ # @param parse_class [String] (Optional) the Parse class type are the items of the collection.
57
+ # This is used to typecast the objects in the array to a particular Parse Object type.
58
+ # @see PointerCollectionProxy
59
+ # @see RelationCollectionProxy
38
60
  def initialize(collection = nil, delegate: nil, key: nil, parse_class: nil)
39
61
  @delegate = delegate
40
62
  @key = key.to_sym if key.present?
@@ -43,21 +65,27 @@ module Parse
43
65
  @parse_class = parse_class
44
66
  end
45
67
 
68
+ # true if the collection has been loaded
46
69
  def loaded?
47
70
  @loaded
48
71
  end
49
72
 
50
- # helper method to forward a message to the delegate
73
+ # Forward a method call to the delegate.
74
+ # @param method [Symbol] the name of the method to forward
75
+ # @param params [Object] method parameters
76
+ # @return [Object] the return value from the forwarded method.
51
77
  def forward(method, params = nil)
52
78
  return unless @delegate.present? && @delegate.respond_to?(method)
53
79
  params.nil? ? @delegate.send(method) : @delegate.send(method, params)
54
80
  end
55
81
 
82
+ # Reset the state of the collection.
56
83
  def reset!
57
84
  @loaded = false
58
85
  clear
59
86
  end
60
87
 
88
+ # @return [Boolean] true if two collection proxies have similar items.
61
89
  def ==(other_list)
62
90
  if other_list.is_a?(Array)
63
91
  return @collection == other_list
@@ -66,25 +94,32 @@ module Parse
66
94
  end
67
95
  end
68
96
 
97
+ # Reload and restore the collection to its original set of items.
69
98
  def reload!
70
99
  reset!
71
100
  collection #force reload
72
101
  end
73
102
 
103
+ # clear all items in the collection
74
104
  def clear
75
105
  @collection.clear
76
106
  end
77
107
 
78
- def to_ary
108
+ # @return [Array]
109
+ def to_a
79
110
  collection.to_a
80
- end; alias_method :to_a, :to_ary
111
+ end; alias_method :to_ary, :to_a
81
112
 
113
+ # @!attribute [rw] collection
114
+ # Set the internal collection of items without dirty tracking or
115
+ # change notifications.
82
116
  def set_collection!(list)
83
117
  @collection = list
84
118
  end
85
119
 
86
- # lazy loading of a collection. If empty and not loaded, then forward _fetch!
87
- # to the delegate
120
+ # @!attribute [rw] collection
121
+ # The internal backing store of the collection.
122
+ # @return [Array] contents of the collection.
88
123
  def collection
89
124
  if @collection.empty? && @loaded == false
90
125
  @collection = forward( :"#{@key}_fetch!" ) || @collection || []
@@ -99,7 +134,8 @@ module Parse
99
134
  @collection = c
100
135
  end
101
136
 
102
- # Method to add items to the collection.
137
+ # Add items to the collection
138
+ # @param items [Array] items to add
103
139
  def add(*items)
104
140
  notify_will_change! if items.count > 0
105
141
  items.each do |item|
@@ -109,6 +145,7 @@ module Parse
109
145
  end; alias_method :push, :add
110
146
 
111
147
  # Remove items from the collection
148
+ # @param items [Array] items to remove
112
149
  def remove(*items)
113
150
  notify_will_change! if items.count > 0
114
151
  items.each do |item|
@@ -117,24 +154,37 @@ module Parse
117
154
  @collection
118
155
  end; alias_method :delete, :remove
119
156
 
157
+ # Atomically adds all items from the array.
158
+ # This request is sent directly to the Parse backend.
159
+ # @param items [Array] items to uniquely add
160
+ # @see #add_unique!
120
161
  def add!(*items)
121
162
  return false unless @delegate.respond_to?(:op_add!)
122
163
  @delegate.send :op_add!, @key, items.flatten
123
164
  reset!
124
165
  end
125
166
 
167
+ # Atomically adds all items from the array that are not already part of the collection.
168
+ # This request is sent directly to the Parse backend.
169
+ # @param items [Array] items to uniquely add
170
+ # @see #add!
126
171
  def add_unique!(*items)
127
172
  return false unless @delegate.respond_to?(:op_add_unique!)
128
173
  @delegate.send :op_add_unique!, @key, items.flatten
129
174
  reset!
130
175
  end
131
176
 
177
+ # Atomically deletes all items from the array. This request is sent
178
+ # directly to the Parse backend.
179
+ # @param items [Array] items to remove
132
180
  def remove!(*items)
133
181
  return false unless @delegate.respond_to?(:op_remove!)
134
182
  @delegate.send :op_remove!, @key, items.flatten
135
183
  reset!
136
184
  end
137
185
 
186
+ # Atomically deletes all items in the array, and marks the field as `undefined` directly
187
+ # with the Parse server. This request is sent directly to the Parse backend.
138
188
  def destroy!
139
189
  return false unless @delegate.respond_to?(:op_destroy!)
140
190
  @delegate.send :op_destroy!, @key
@@ -143,30 +193,39 @@ module Parse
143
193
  reset!
144
194
  end
145
195
 
196
+ # Locally restores previous attributes (not from the persistent store)
146
197
  def rollback!
147
198
  restore_attributes
148
199
  end
149
200
 
201
+ # clears all dirty tracked information.
150
202
  def clear_changes!
151
203
  clear_changes_information
152
204
  end
153
205
 
206
+ # mark that collection changes where applied, which clears dirty tracking.
154
207
  def changes_applied!
155
208
  changes_applied
156
209
  end
157
210
 
211
+ # @param args [Hash] arguments to pass to Array#first.
212
+ # @return [Object] the first item in the collection
158
213
  def first(*args)
159
214
  collection.first(*args)
160
215
  end
161
216
 
217
+ # @return [Object] the second item in the collection
162
218
  def second
163
219
  collection.second
164
220
  end
165
221
 
222
+ # @param args [Hash] arguments to pass to Array#last.
223
+ # @return [Object] the last item in the collection
166
224
  def last(*args)
167
225
  collection.last(*args)
168
226
  end
169
227
 
228
+ # @return [Integer] number of items in the collection.
170
229
  def count
171
230
  collection.count
172
231
  end
@@ -175,24 +234,25 @@ module Parse
175
234
  collection.as_json(args)
176
235
  end
177
236
 
237
+ # true if the collection is empty.
178
238
  def empty?
179
239
  collection.empty?
180
240
  end
181
241
 
182
- # append items to the array
242
+ # Append items to the collection
183
243
  def <<(*list)
184
244
  if list.count > 0
185
245
  notify_will_change!
186
246
  list.flatten.each { |e| collection.push(e) }
187
247
  end
188
248
  end
189
- # we call our own dirty tracking and also forward the call
249
+
250
+ # Notifies the delegate that the collection changed.
190
251
  def notify_will_change!
191
252
  collection_will_change!
192
253
  forward "#{@key}_will_change!"
193
254
  end
194
255
 
195
- # supported iterator
196
256
  def each
197
257
  return collection.enum_for(:each) unless block_given?
198
258
  collection.each &Proc.new
@@ -10,29 +10,291 @@ module Parse
10
10
 
11
11
  module Associations
12
12
 
13
- # This module provides has_many functionality to defining Parse::Object classes.
14
- # There are two main types of a has_many association - array and relation.
15
- # A has_many array relation, uses a PointerCollectionProxy to store a list of Parse::Object (or pointers)
16
- # that are stored in the column of the local table. This means we expect a the remote Parse table to contain
17
- # a column of type array which would contain a set of hash-like Pointers.
18
- # In the relation case, the object's Parse table has a column, but it points to a separate
19
- # relational table (join table) which maps both the local class and the foreign class. In this case
20
- # the type of the column is of "Relation" with a specific class name. This then means that it contains a set of
21
- # object Ids that we will treat as being part of the foreign table.
22
- # Ex. If a Artist defines a has_many relation to a Song class through a column called 'favoriteSongs'.
23
- # Then the Parse type of the favoriteSongs column, contained in the Artist table,
24
- # would be Relation<Song>. Any objectIds listed in that relation would then
25
- # be Song object Ids.
26
- # One thing to note is that when querying relations, the foreign table is the one that needs to be
27
- # queried in order to retrive the associated object to the local object. For example,
28
- # if an Artist has a relation to many Song objects, and we wanted to get the list of songs
29
- # this artist is related to, we would query the Song table passing the specific artist record
30
- # we are constraining to.
13
+ # Parse has many ways to implement one-to-many and many-to-many
14
+ # associations: `Array`, `Parse Relation` or through a `Query`. How you decide
15
+ # to implement your associations, will affect how `has_many` works in
16
+ # Parse-Stack. Parse natively supports one-to-many and many-to-many
17
+ # relationships using `Array` and `Relations`, as described in
18
+ # {https://parseplatform.github.io/docs/js/guide/#relational-data Parse Relational Data}.
19
+ # Both of these methods require you define a specific column type in your
20
+ # Parse table that will be used to store information about the association.
21
+ #
22
+ # In addition to `Array` and `Relation`, Parse-Stack also implements the
23
+ # standard `has_many` behavior prevalent in other frameworks through a query
24
+ # where the associated class contains a foreign pointer to the local class,
25
+ # usually the inverse of a `belongs_to`. This requires that the associated
26
+ # class has a defined column that contains a pointer the refers to the
27
+ # defining class.
28
+ #
29
+ # *Query-Approach*
30
+ #
31
+ # In this `Query` implementation, a `has_many` association for a Parse class
32
+ # requires that another Parse class will have a foreign pointer that refers
33
+ # to instances of this class. This is the standard way that `has_many`
34
+ # relationships work in most databases systems. This is usually the case when
35
+ # you have a class that has a `belongs_to` relationship to instances of the
36
+ # local class.
37
+ #
38
+ # In the example below, many songs belong to a specific artist. We set this
39
+ # association by setting {Associations::BelongsTo.belongs_to :belongs_to} relationship from `Song` to `Artist`.
40
+ # Knowing there is a column in `Song` that points to instances of an `Artist`,
41
+ # we can setup a `has_many` association to `Song` instances in the `Artist`
42
+ # class. Doing so will generate a helper query method on the `Artist` instance
43
+ # objects.
44
+ #
45
+ # class Song < Parse::Object
46
+ # property :released, :date
47
+ # # this class will have a pointer column to an Artist
48
+ # belongs_to :artist
49
+ # end
50
+ #
51
+ # class Artist < Parse::Object
52
+ # has_many :songs
53
+ # end
54
+ #
55
+ # artist = Artist.first
56
+ #
57
+ # artist.songs # => [all songs belonging to artist]
58
+ # # equivalent: Song.all(artist: artist)
59
+ #
60
+ # # filter also by release date
61
+ # artist.songs(:released.after => 1.year.ago)
62
+ # # equivalent: Song.all(artist: artist, :released.after => 1.year.ago)
63
+ #
64
+ # In order to modify the associated objects (ex. `songs`), you must modify
65
+ # their corresponding `belongs_to` field (in this case `song.artist`), to
66
+ # another record and save it.
67
+ #
68
+ # Options for `has_many` using the `Query` approach are `:as` and `:field`. The
69
+ # `:as` option behaves similarly to the {Associations::BelongsTo.belongs_to :belongs_to} counterpart. The
70
+ # `:field` option can be used to override the derived column name located
71
+ # in the foreign class. The default value for `:field` is the columnized
72
+ # version of the Parse subclass `parse_class` method.
73
+ #
74
+ # class Parse::User
75
+ # # since the foreign column name is :agent
76
+ # has_many :artists, field: :agent
77
+ # end
78
+ #
79
+ # class Artist < Parse::Object
80
+ # belongs_to :manager, as: :user, field: :agent
81
+ # end
82
+ #
83
+ # artist.manager # => Parse::User object
84
+ #
85
+ # user.artists # => [artists where :agent column is user]
86
+ #
87
+ #
88
+ # When using this approach, you may also employ the use of scopes to filter the particular data from the `has_many` association.
89
+ #
90
+ # class Artist
91
+ # has_many :songs, ->(timeframe) { where(:created_at.after => timeframe) }
92
+ # end
93
+ #
94
+ # artist.songs(6.months.ago)
95
+ # # => [artist's songs created in the last 6 months]
96
+ #
97
+ # You may also call property methods in your scopes related to the instance.
98
+ # You also have access to the instance object for the local class
99
+ # through a special "*i*" method in the scope.
100
+ #
101
+ # class Concert
102
+ # property :city
103
+ # belongs_to :artist
104
+ # end
105
+ #
106
+ # class Artist
107
+ # property :hometown
108
+ # has_many :local_concerts, -> { where(:city => hometown) }, as: :concerts
109
+ # end
110
+ #
111
+ # # assume
112
+ # artist.hometown = "San Diego"
113
+ #
114
+ # # artist's concerts in their hometown of 'San Diego'
115
+ # artist.local_concerts
116
+ # # equivalent: Concert.all(artist: artist, city: artist.hometown)
117
+ #
118
+ # You may also omit the association completely, as rely on the scope to fetch the
119
+ # associated records. This makes the `has_many` work as a macro query setting the :scope_only
120
+ # option to true:
121
+ #
122
+ # class Author < Parse::Object
123
+ # property :name
124
+ # has_many :posts, ->{ where :tags.in => name.downcase }, scope_only: true
125
+ # end
126
+ #
127
+ # class Post < Parse::Object
128
+ # property :tags, :array
129
+ # end
130
+ #
131
+ # author.posts # => Posts where author's name is a tag
132
+ # # equivalent: Post.all( :tags.in => artist.name.downcase )
133
+ #
134
+ # *Array-Approach*
135
+ #
136
+ # In the `Array` implemenatation, you can designate a column to be of `Array`
137
+ # type that contains a list of Parse pointers. Parse-Stack supports this by
138
+ # passing the option `through: :array` to the `has_many` method. If you use
139
+ # this approach, it is recommended that this is used for associations where
140
+ # the quantity is less than 100 in order to maintain query and fetch
141
+ # performance. You would be in charge of maintaining the array with the proper
142
+ # list of Parse pointers that are associated to the object. Parse-Stack does
143
+ # help by wrapping the array in a {Parse::PointerCollectionProxy} which provides dirty tracking.
144
+ #
145
+ # class Artist < Parse::Object
146
+ # end
147
+ #
148
+ # class Band < Parse::Object
149
+ # has_many :artists, through: :array
150
+ # end
151
+ #
152
+ # artist = Artist.first
153
+ #
154
+ # # find all bands that contain this artist
155
+ # bands = Band.all( :artists.in => [artist.pointer] )
156
+ #
157
+ # band = bands.first
158
+ # band.artists # => [array of Artist pointers]
159
+ #
160
+ # # remove artists
161
+ # band.artists.remove artist
162
+ #
163
+ # # add artist
164
+ # band.artists.add artist
165
+ #
166
+ # # save changes
167
+ # band.save
168
+ #
169
+ # *ParseRelation-Approach*
170
+ #
171
+ # Other than the use of arrays, Parse supports native one-to-many and many-to-many
172
+ # associations through what is referred to as a {https://parseplatform.github.io/docs/js/guide/#many-to-many-relationships Parse Relation}.
173
+ # This is implemented by defining a column to be of type `Relation` which
174
+ # refers to a foreign class. Parse-Stack supports this by passing the
175
+ # `through: :relation` option to the `has_many` method. Designating a column
176
+ # as a Parse relation to another class type, will create a one-way intermediate
177
+ # "join-list" between the local class and the foreign class. One important
178
+ # distinction of this compared to other types of data stores (ex. PostgresSQL) is that:
179
+ #
180
+ # *1*. The inverse relationship association is not available automatically. Therefore, having a column of `artists` in a `Band` class that relates to members of the band (as `Artist` class), does not automatically make a set of `Band` records available to `Artist` records for which they have been related. If you need to maintain both the inverse relationship between a foreign class to its associations, you will need to manually manage that by adding two Parse relation columns in each class, or by creating a separate class (ex. `ArtistBands`) that is used as a join table.
181
+ #
182
+ # *2*. Querying the relation is actually performed against the implicit join table, not the local one.
183
+ #
184
+ # *3*. Applying query constraints for a set of records within a relation is performed against the foreign table class, not the class having the relational column.
185
+ #
186
+ # The Parse documentation provides more details on associations, see {http://parseplatform.github.io/docs/ios/guide/#relations Parse Relations Guide}.
187
+ # Parse-Stack will handle the work for (2) and (3) automatically.
188
+ #
189
+ # In the example below, a `Band` can have thousands of `Fans`. We setup a
190
+ # `Relation<Fan>` column in the `Band` class that references the `Fan` class.
191
+ # Parse-Stack provides methods to manage the relationship under the {Parse::RelationCollectionProxy}
192
+ # class.
193
+ #
194
+ # class Fan < Parse::Object
195
+ # # .. lots of properties ...
196
+ # property :location, :geopoint
197
+ # end
198
+ #
199
+ # class Band < Parse::Object
200
+ # has_many :fans, through: :relation 
201
+ # end
202
+ #
203
+ # band = Band.first
204
+ #
205
+ # # the number of fans in the relation
206
+ # band.fans.count
207
+ #
208
+ # # get the first object in relation
209
+ # fan = bands.fans.first # => Parse::User object
210
+ #
211
+ # # use `add` or `remove` to modify relations
212
+ # band.fans.add user
213
+ # bands.fans.remove user
214
+ #
215
+ # # updates the relation as well as changes to `band`
216
+ # band.fans.save
217
+ #
218
+ # # Find 50 fans who are near San Diego, CA
219
+ # downtown = Parse::GeoPoint.new(32.82, -117.23)
220
+ # fans = band.fans.all :location.near => downtown
221
+ #
222
+ # You can perform atomic additions and removals of objects from `has_many`
223
+ # relations. Parse allows this by providing a specific atomic operation
224
+ # request. You can use the methods below to perform these types of atomic
225
+ # operations. The operation is performed directly on Parse server
226
+ # and not on your instance object.
227
+ #
228
+ # # atomically add/remove
229
+ # band.artists.add! objects # { __op: :AddUnique }
230
+ # band.artists.remove! objects # { __op: :AddUnique }
231
+ #
232
+ # # atomically add unique Artist
233
+ # band.artists.add_unique! objects # { __op: :AddUnique }
234
+ #
235
+ # # atomically add/remove relations
236
+ # band.fans.add! users # { __op: :Add }
237
+ # band.fans.remove! users # { __op: :Remove }
238
+ #
239
+ # # atomically perform a delete operation on this field name
240
+ # # this should set it as `undefined`.
241
+ # band.op_destroy!("category") # { __op: :Delete }
242
+ #
31
243
  module HasMany
244
+
245
+ # @!attribute [rw] self.relations
246
+ # A hash mapping of all has_many associations that use the ParseRelation implementation.
247
+ # @return [Hash]
248
+
249
+ # Define a one-to-many or many-to-many association between the local model and a foreign class.
250
+ # Options for `has_many` are the same as the {Associations::BelongsTo.belongs_to} counterpart with
251
+ # support for `:required`, `:as` and `:field`. It has additional options.
252
+ #
253
+ # @!method self.has_many(key, scope = nil, opts = {})
254
+ # @param [Symbol] key The pluralized version of the foreign class. Using the :query method,
255
+ # this implies the name of the foreign column that a pointer to this record.
256
+ # Using the :array or :relation method, this implies the name of the local
257
+ # column that contains either an array of Parse::Pointers in the case of :array,
258
+ # or the Parse Relation, in the case of :relation.
259
+ # @param [Proc] scope Only applicable using :query. A proc that can customize the query by applying
260
+ # additional constraints when fetching the associated records. Works similarly as
261
+ # ActiveModel associations described in section {http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html Customizing the Query}
262
+ # @option opts [Symbol] :through The type of implementation to use: :query (default), :array or :relation.
263
+ # If set to `:array`, it defines the column in Parse as being an array of
264
+ # Parse pointer objects and will be managed locally using a {Parse::PointerCollectionProxy}.
265
+ # If set to `:relation`, it defines a column of type Parse Relation with
266
+ # the foreign class and will be managed locally using a {Parse::RelationCollectionProxy}.
267
+ # If set to `:query`, no storage is required on the local class as the
268
+ # associated records will be fetched using a Parse query.
269
+ # @option opts [Symbol] :field override the name of the remote column to use when fetching the association.
270
+ # When using through :query, this is the column name of the remote column
271
+ # of the foreign class that will be used for matching. When using :array,
272
+ # this is the name of the remote column of the local class that contains
273
+ # an array of pointers to the foreign class. When using :relation, this
274
+ # is the name of the remote column of the local class that contains the Parse Relation.
275
+ # @option opts [Symbol] :as override the inferred Parse::Object subclass of the association.
276
+ # By default this is inferred as the singularized camel case version of
277
+ # the key parameter. This option allows you to override the typecast of
278
+ # foreign Parse model of the association, while allowing you to have a
279
+ # different accessor name.
280
+ # @example
281
+ # has_many :fans, as: :users, through: :relation, field: "awesomeFans"
282
+ # has_many :songs
283
+ # has_many :likes, as: :users, through: :relation
284
+ # has_many :artists, field: "managedArtists"
285
+ #
286
+ # @return [Array<Parse::Object>] if through :query
287
+ # @return [PointerCollectionProxy] if through :array
288
+ # @return [RelationCollectionProxy] if through :relation
289
+ # @see PointerCollectionProxy
290
+ # @see RelationCollectionProxy
291
+
292
+ # @!visibility private
32
293
  def self.included(base)
33
294
  base.extend(ClassMethods)
34
295
  end
35
296
 
297
+ # @!visibility private
36
298
  module ClassMethods
37
299
  attr_accessor :relations
38
300
  def relations
@@ -89,17 +351,24 @@ module Parse
89
351
  query.conditions(*args)
90
352
  end
91
353
 
92
- query.define_singleton_method(:method_missing) do |m, *args, &block|
354
+ query.define_singleton_method(:method_missing) do |m, *args, &chained_block|
93
355
  klass = Parse::Model.find_class klassName
356
+
94
357
  if klass.present? && klass.respond_to?(m)
95
- klass_scope = klass.send(m, *args, &block)
96
- return klass_scope.is_a?(Parse::Query) ?
97
- self.add_constraints( klass_scope.constraints ) :
98
- klass_scope
358
+
359
+ klass_scope = klass.send(m, *args) # blocks only passed to final result set
360
+ return klass_scope unless klass_scope.is_a?(Parse::Query)
361
+ # merge constraints
362
+ add_constraints( klass_scope.constraints )
363
+ # if a block was passed, execute the query, otherwise return the query
364
+ return chained_block.present? ? results(&chained_block) : self
99
365
  end
100
- self.results.send(m, *args, &block)
366
+ results.send(m, *args, &chained_block)
101
367
  end
102
368
 
369
+ query.define_singleton_method(:to_s) { self.results.to_s }
370
+ query.define_singleton_method(:inspect) { self.results.to_a.inspect }
371
+
103
372
  return query if block.nil?
104
373
  query.results(&block)
105
374
  end
@@ -255,13 +524,16 @@ module Parse
255
524
  end # has_many_array
256
525
  end #ClassMethods
257
526
 
258
- # provides a hash list of all relations to this class.
527
+ # A hash list of all has_many associations that use a Parse Relation.
528
+ # @return [Hash]
529
+ # @see Associations::HasMany.relations
259
530
  def relations
260
531
  self.class.relations
261
532
  end
262
533
 
263
- # returns a hash of all the relation changes that have been performed on this
264
- # instance.
534
+ # A hash of all the relation changes that have been performed on this
535
+ # instance. This is only used when the association uses Parse Relations.
536
+ # @return [Hash]
265
537
  def relation_updates
266
538
  h = {}
267
539
  changed.each do |key|
@@ -272,7 +544,8 @@ module Parse
272
544
  h
273
545
  end
274
546
 
275
- # true if this object has any relation changes
547
+ # @return [Boolean] true if there are pending relational changes for
548
+ # associations defined using Parse Relations.
276
549
  def relation_changes?
277
550
  changed.any? { |key| relations[key.to_sym] }
278
551
  end