parse-stack 1.4.3 → 1.5.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +52 -39
  3. data/Gemfile.lock +2 -2
  4. data/README.md +609 -124
  5. data/bin/console +0 -9
  6. data/lib/parse/api/all.rb +3 -0
  7. data/lib/parse/api/analytics.rb +2 -2
  8. data/lib/parse/api/apps.rb +15 -17
  9. data/lib/parse/api/batch.rb +4 -1
  10. data/lib/parse/api/cloud_functions.rb +2 -0
  11. data/lib/parse/api/config.rb +14 -2
  12. data/lib/parse/api/files.rb +6 -3
  13. data/lib/parse/api/hooks.rb +4 -4
  14. data/lib/parse/api/objects.rb +14 -11
  15. data/lib/parse/api/push.rb +4 -2
  16. data/lib/parse/api/schemas.rb +6 -5
  17. data/lib/parse/api/sessions.rb +11 -1
  18. data/lib/parse/api/users.rb +65 -15
  19. data/lib/parse/client/authentication.rb +4 -2
  20. data/lib/parse/client/body_builder.rb +11 -3
  21. data/lib/parse/client/caching.rb +17 -6
  22. data/lib/parse/client/protocol.rb +14 -8
  23. data/lib/parse/client/request.rb +4 -1
  24. data/lib/parse/client/response.rb +59 -6
  25. data/lib/parse/client.rb +72 -42
  26. data/lib/parse/model/acl.rb +22 -4
  27. data/lib/parse/model/associations/belongs_to.rb +22 -10
  28. data/lib/parse/model/associations/collection_proxy.rb +14 -1
  29. data/lib/parse/model/associations/has_many.rb +76 -15
  30. data/lib/parse/model/associations/has_one.rb +69 -0
  31. data/lib/parse/model/associations/pointer_collection_proxy.rb +13 -6
  32. data/lib/parse/model/associations/relation_collection_proxy.rb +5 -2
  33. data/lib/parse/model/bytes.rb +6 -2
  34. data/lib/parse/model/classes/installation.rb +27 -0
  35. data/lib/parse/model/classes/role.rb +20 -0
  36. data/lib/parse/model/classes/session.rb +26 -0
  37. data/lib/parse/model/classes/user.rb +185 -0
  38. data/lib/parse/model/core/actions.rb +40 -26
  39. data/lib/parse/model/core/properties.rb +126 -20
  40. data/lib/parse/model/core/querying.rb +63 -3
  41. data/lib/parse/model/core/schema.rb +9 -6
  42. data/lib/parse/model/date.rb +5 -1
  43. data/lib/parse/model/file.rb +12 -9
  44. data/lib/parse/model/geopoint.rb +6 -4
  45. data/lib/parse/model/model.rb +29 -21
  46. data/lib/parse/model/object.rb +29 -76
  47. data/lib/parse/model/pointer.rb +8 -6
  48. data/lib/parse/model/push.rb +4 -1
  49. data/lib/parse/query/constraint.rb +3 -0
  50. data/lib/parse/query/constraints.rb +6 -3
  51. data/lib/parse/query/operation.rb +3 -0
  52. data/lib/parse/query/ordering.rb +3 -0
  53. data/lib/parse/query.rb +85 -38
  54. data/lib/parse/stack/generators/rails.rb +3 -0
  55. data/lib/parse/stack/railtie.rb +2 -0
  56. data/lib/parse/stack/tasks.rb +4 -1
  57. data/lib/parse/stack/version.rb +4 -1
  58. data/lib/parse/stack.rb +3 -0
  59. data/lib/parse/webhooks/payload.rb +14 -8
  60. data/lib/parse/webhooks/registration.rb +11 -8
  61. data/lib/parse/webhooks.rb +11 -8
  62. data/lib/parse-stack.rb +3 -0
  63. data/parse-stack.gemspec +10 -8
  64. metadata +16 -4
@@ -1,3 +1,52 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ # Member name Value Description
5
+ # OtherCause -1 Error code indicating that an unknown error or an error unrelated to Parse occurred.
6
+ # InternalServerError 1 Error code indicating that something has gone wrong with the server. If you get this error code, it is Parse's fault. Please report the bug to https://parse.com/help.
7
+ # ConnectionFailed 100 Error code indicating the connection to the Parse servers failed.
8
+ # ObjectNotFound 101 Error code indicating the specified object doesn't exist.
9
+ # InvalidQuery 102 Error code indicating you tried to query with a datatype that doesn't support it, like exact matching an array or object.
10
+ # InvalidClassName 103 Error code indicating a missing or invalid classname. Classnames are case-sensitive. They must start with a letter, and a-zA-Z0-9_ are the only valid characters.
11
+ # MissingObjectId 104 Error code indicating an unspecified object id.
12
+ # InvalidKeyName 105 Error code indicating an invalid key name. Keys are case-sensitive. They must start with a letter, and a-zA-Z0-9_ are the only valid characters.
13
+ # InvalidPointer 106 Error code indicating a malformed pointer. You should not see this unless you have been mucking about changing internal Parse code.
14
+ # InvalidJSON 107 Error code indicating that badly formed JSON was received upstream. This either indicates you have done something unusual with modifying how things encode to JSON, or the network is failing badly.
15
+ # CommandUnavailable 108 Error code indicating that the feature you tried to access is only available internally for testing purposes.
16
+ # NotInitialized 109 You must call Parse.initialize before using the Parse library.
17
+ # IncorrectType 111 Error code indicating that a field was set to an inconsistent type.
18
+ # InvalidChannelName 112 Error code indicating an invalid channel name. A channel name is either an empty string (the broadcast channel) or contains only a-zA-Z0-9_ characters and starts with a letter.
19
+ # PushMisconfigured 115 Error code indicating that push is misconfigured.
20
+ # ObjectTooLarge 116 Error code indicating that the object is too large.
21
+ # OperationForbidden 119 Error code indicating that the operation isn't allowed for clients.
22
+ # CacheMiss 120 Error code indicating the result was not found in the cache.
23
+ # InvalidNestedKey 121 Error code indicating that an invalid key was used in a nested JSONObject.
24
+ # InvalidFileName 122 Error code indicating that an invalid filename was used for ParseFile. A valid file name contains only a-zA-Z0-9_. characters and is between 1 and 128 characters.
25
+ # InvalidACL 123 Error code indicating an invalid ACL was provided.
26
+ # Timeout 124 Error code indicating that the request timed out on the server. Typically this indicates that the request is too expensive to run.
27
+ # InvalidEmailAddress 125 Error code indicating that the email address was invalid.
28
+ # DuplicateValue 137 Error code indicating that a unique field was given a value that is already taken.
29
+ # InvalidRoleName 139 Error code indicating that a role's name is invalid.
30
+ # ExceededQuota 140 Error code indicating that an application quota was exceeded. Upgrade to resolve.
31
+ # ScriptFailed 141 Error code indicating that a Cloud Code script failed.
32
+ # ValidationFailed 142 Error code indicating that a Cloud Code validation failed.
33
+ # FileDeleteFailed 153 Error code indicating that deleting a file failed.
34
+ # RequestLimitExceeded 155 Error code indicating that the application has exceeded its request limit.
35
+ # InvalidEventName 160 Error code indicating that the provided event name is invalid.
36
+ # UsernameMissing 200 Error code indicating that the username is missing or empty.
37
+ # PasswordMissing 201 Error code indicating that the password is missing or empty.
38
+ # UsernameTaken 202 Error code indicating that the username has already been taken.
39
+ # EmailTaken 203 Error code indicating that the email has already been taken.
40
+ # EmailMissing 204 Error code indicating that the email is missing, but must be specified.
41
+ # EmailNotFound 205 Error code indicating that a user with the specified email was not found.
42
+ # SessionMissing 206 Error code indicating that a user object without a valid session could not be altered.
43
+ # MustCreateUserThroughSignup 207 Error code indicating that a user can only be created through signup.
44
+ # AccountAlreadyLinked 208 Error code indicating that an an account being linked is already linked to another user.
45
+ # InvalidSessionToken 209 Error code indicating that the current session token is invalid.
46
+ # LinkedIdMissing 250 Error code indicating that a user cannot be linked to an account because that account's id could not be found.
47
+ # InvalidLinkedSession 251 Error code indicating that a user with a linked (e.g. Facebook) account has an invalid session.
48
+ # UnsupportedService 252 Error code indicating that a service being linked (e.g. Facebook or Twitter) is unsupported.
49
+
1
50
  require 'active_support'
2
51
  require 'active_support/json'
3
52
  # This is the model that represents a response from Parse. A Response can also
@@ -13,11 +62,15 @@ module Parse
13
62
  ERROR_TIMEOUT = 124
14
63
  ERROR_EXCEEDED_BURST_LIMIT = 155
15
64
  ERROR_OBJECT_NOT_FOUND = 101
16
-
17
- ERROR = "error".freeze
18
- CODE = "code".freeze
19
- RESULTS = "results".freeze
20
- COUNT = "count".freeze
65
+ ERROR_USERNAME_MISSING = 200
66
+ ERROR_PASSWORD_MISSING = 201
67
+ ERROR_USERNAME_TAKEN = 202
68
+ ERROR_EMAIL_TAKEN = 203
69
+
70
+ ERROR = "error"
71
+ CODE = "code"
72
+ RESULTS = "results"
73
+ COUNT = "count"
21
74
  # A response has a result or (a code and an error)
22
75
  attr_accessor :parse_class, :code, :error, :result, :http_status
23
76
  attr_accessor :request # capture request that created result
@@ -67,7 +120,7 @@ module Parse
67
120
  # if batch response, generate array based on the response hash.
68
121
  @result.map do |r|
69
122
  next r unless r.is_a?(Hash)
70
- hash = r["success".freeze] || r["error".freeze]
123
+ hash = r["success"] || r[ERROR]
71
124
  Parse::Response.new hash
72
125
  end
73
126
  end
data/lib/parse/client.rb CHANGED
@@ -19,22 +19,34 @@ require_relative "api/all"
19
19
  module Parse
20
20
 
21
21
  # This is an exception that is thrown if there is a client connectivity issue
22
- class ConnectionError < Exception; end;
23
- class TimeoutError < Exception; end;
24
- class ProtocolError < Exception; end;
25
- class ServerError < Exception; end;
26
- class ServiceUnavailableError < Exception; end;
27
- class AuthenticationError < Exception; end;
28
- class RequestLimitExceededError < Exception; end;
29
- class InvalidSessionTokenError < Exception; end;
22
+ class ConnectionError < StandardError; end;
23
+ class TimeoutError < StandardError; end;
24
+ class ProtocolError < StandardError; end;
25
+ class ServerError < StandardError; end;
26
+ class ServiceUnavailableError < StandardError; end;
27
+ class AuthenticationError < StandardError; end;
28
+ class RequestLimitExceededError < StandardError; end;
29
+ class InvalidSessionTokenError < StandardError; end;
30
30
 
31
31
  # helper method to get the config variables.
32
32
  def self.config(s = :default)
33
- Parse::Client.session(s).config
33
+ Parse::Client.client(s).config
34
+ end
35
+
36
+ def self.set_config(field, value, s = :default)
37
+ Parse::Client.client(s).update_config({ field => value })
38
+ end
39
+
40
+ def self.update_config(params, s = :default)
41
+ Parse::Client.client(s).update_config(params)
34
42
  end
35
43
 
36
44
  def self.config!(s = :default)
37
- Parse::Client.session(s).config!
45
+ Parse::Client.client(s).config!
46
+ end
47
+
48
+ def self.client(conn = :default)
49
+ Parse::Client.client(conn)
38
50
  end
39
51
 
40
52
  # Main class for the client. The client class is based on a Faraday stack.
@@ -61,23 +73,37 @@ module Parse
61
73
  # The client can support multiple sessions. The first session created, will be placed
62
74
  # under the default session tag. The :default session will be the default client to be used
63
75
  # by the other classes including Parse::Query and Parse::Objects
64
- @@sessions = { default: nil }
76
+ @clients = { default: nil }
77
+ class << self
78
+ attr_reader :clients
79
+ def session?(v = :default)
80
+ puts '[Warning] Parse::Client#session is DEPRECATED. Please use Parse::Client#client instead.'
81
+ self.client? v
82
+ end
65
83
 
66
- def self.session?(v = :default)
67
- @@sessions[v].present?
68
- end
69
- # get a session for a given tag. This will also create a new one for the tag if not specified.
70
- def self.session(connection = :default)
71
- #warn "Please call Parse::Client.setup() to initialize your parse session" if @@sessions.empty? || @@sessions[:default].nil?
72
- @@sessions[connection] ||= self.new
73
- end
84
+ # DEPRECATED
85
+ # get a session for a given tag. This will also create a new one for the tag if not specified.
86
+ def session(connection = :default)
87
+ puts '[Warning] Parse::Client#session is DEPRECATED. Please use Parse::Client#client instead.'
88
+ self.client(connection)
89
+ end
90
+
91
+ def client?(v = :default)
92
+ @clients[v].present?
93
+ end
94
+
95
+ def client(connection = :default)
96
+ @clients[connection] ||= self.new
97
+ end
98
+
99
+ def setup(opts = {})
100
+ # If Proc.new is called from inside a method without any arguments of
101
+ # its own, it will return a new Proc containing the block given to
102
+ # its surrounding method.
103
+ # http://mudge.name/2011/01/26/passing-blocks-in-ruby-without-block.html
104
+ @clients[:default] = self.new(opts, &Proc.new)
105
+ end
74
106
 
75
- def self.setup(opts = {})
76
- # If Proc.new is called from inside a method without any arguments of
77
- # its own, it will return a new Proc containing the block given to
78
- # its surrounding method.
79
- # http://mudge.name/2011/01/26/passing-blocks-in-ruby-without-block.html
80
- @@sessions[:default] = self.new(opts, &Proc.new)
81
107
  end
82
108
 
83
109
  # This builds a new Parse::Client stack. The options are:
@@ -108,7 +134,7 @@ module Parse
108
134
  #Configure Faraday
109
135
  opts[:faraday] ||= {}
110
136
  opts[:faraday].merge!(:url => @server_url)
111
- @session = Faraday.new(opts[:faraday]) do |conn|
137
+ @conn = Faraday.new(opts[:faraday]) do |conn|
112
138
  #conn.request :json
113
139
 
114
140
  conn.response :logger if opts[:logging]
@@ -134,7 +160,7 @@ module Parse
134
160
 
135
161
  if opts[:cache].present? && opts[:expires].to_i > 0
136
162
  unless opts[:cache].is_a?(Moneta::Transformer)
137
- raise "Parse::Client option :cache needs to be a type of Moneta::Transformer store."
163
+ raise ArgumentError, "Parse::Client option :cache needs to be a type of Moneta::Transformer store."
138
164
  end
139
165
  self.cache = opts[:cache]
140
166
  conn.use Parse::Middleware::Caching, self.cache, {expires: opts[:expires].to_i }
@@ -145,7 +171,7 @@ module Parse
145
171
  conn.adapter opts[:adapter]
146
172
 
147
173
  end
148
- @@sessions[:default] ||= self
174
+ Parse::Client.clients[:default] ||= self
149
175
  self
150
176
  end
151
177
 
@@ -154,7 +180,7 @@ module Parse
154
180
  end
155
181
 
156
182
  def url_prefix
157
- @session.url_prefix
183
+ @conn.url_prefix
158
184
  end
159
185
 
160
186
  def clear_cache!
@@ -189,10 +215,10 @@ module Parse
189
215
  # http method
190
216
  method = method.downcase.to_sym
191
217
  # set the User-Agent
192
- headers["User-Agent".freeze] = "Parse-Server Ruby Client v#{Parse::Stack::VERSION}".freeze
218
+ headers["User-Agent"] = "Parse-Stack v#{Parse::Stack::VERSION}"
193
219
 
194
220
  if opts[:cache] == false
195
- headers[Parse::Middleware::Caching::CACHE_CONTROL] = "no-cache".freeze
221
+ headers[Parse::Middleware::Caching::CACHE_CONTROL] = "no-cache"
196
222
  elsif opts[:cache].is_a?(Numeric)
197
223
  # specify the cache duration of this request
198
224
  headers[Parse::Middleware::Caching::CACHE_EXPIRES_DURATION] = opts[:cache].to_i
@@ -202,16 +228,18 @@ module Parse
202
228
  headers[Parse::Middleware::Authentication::DISABLE_MASTER_KEY] = "true"
203
229
  end
204
230
 
205
- if opts[:session_token].present?
231
+ token = opts[:session_token]
232
+ if token.present?
233
+ token = token.session_token if token.respond_to?(:session_token)
206
234
  headers[Parse::Middleware::Authentication::DISABLE_MASTER_KEY] = "true"
207
- headers[Parse::Protocol::SESSION_TOKEN] = opts[:session_token]
235
+ headers[Parse::Protocol::SESSION_TOKEN] = token
208
236
  end
209
237
 
210
238
  #if it is a :get request, then use query params, otherwise body.
211
239
  params = (method == :get ? query : body) || {}
212
240
  # if the path does not start with the '/1/' prefix, then add it to be nice.
213
241
  # actually send the request and return the body
214
- response_env = @session.send(method, uri, params, headers)
242
+ response_env = @conn.send(method, uri, params, headers)
215
243
  response = response_env.body
216
244
  response.request = _request
217
245
 
@@ -296,7 +324,7 @@ module Parse
296
324
  end
297
325
 
298
326
  def send_request(req) #Parse::Request object
299
- raise "Object not of Parse::Request type." unless req.is_a?(Parse::Request)
327
+ raise ArgumentError, "Object not of Parse::Request type." unless req.is_a?(Parse::Request)
300
328
  request req.method, req.path, req.body, req.headers
301
329
  end
302
330
 
@@ -313,7 +341,7 @@ module Parse
313
341
  module ClassMethods
314
342
  attr_accessor :client
315
343
  def client
316
- @client ||= Parse::Client.session #defaults to :default tag
344
+ @client ||= Parse::Client.client #defaults to :default tag
317
345
  end
318
346
  end
319
347
 
@@ -335,16 +363,18 @@ module Parse
335
363
  end
336
364
 
337
365
  # Helper method to call cloud functions and get results
338
- def self.trigger_job(name, body, session: :default, raw: false)
339
- response = Parse::Client.session(session).trigger_job(name, body)
340
- return response if raw
366
+ def self.trigger_job(name, body = {}, **opts)
367
+ conn = opts[:session] || opts[:client] || :default
368
+ response = Parse::Client.client(conn).trigger_job(name, body)
369
+ return response if opts[:raw].present?
341
370
  response.error? ? nil : response.result["result"]
342
371
  end
343
372
 
344
373
  # Helper method to call cloud functions and get results
345
- def self.call_function(name, body, session: :default, raw: false)
346
- response = Parse::Client.session(session).call_function(name, body)
347
- return response if raw
374
+ def self.call_function(name, body = {}, **opts)
375
+ conn = opts[:session] || opts[:client] || :default
376
+ response = Parse::Client.client(conn).call_function(name, body)
377
+ return response if opts[:raw].present?
348
378
  response.error? ? nil : response.result["result"]
349
379
  end
350
380
 
@@ -1,3 +1,5 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
1
3
 
2
4
  # An ACL represents the Parse Permissions object used for each record. In Parse,
3
5
  # it is composed a hash-like object that represent Parse::User objectIds and/or Parse::Role
@@ -12,12 +14,28 @@
12
14
  # of dirty tracking.
13
15
  module Parse
14
16
 
15
- class ACL
16
- # The internal permissions hash and delegate accessors
17
- attr_accessor :permissions, :delegate
17
+ # Base class for supporting custom data types
18
+ class DataType
18
19
  include ::ActiveModel::Model
19
20
  include ::ActiveModel::Serializers::JSON
20
- PUBLIC = "*".freeze # Public priviledges are '*' key in Parse
21
+ def attributes
22
+ {}
23
+ end
24
+
25
+ def self.typecast(value, **opts)
26
+ value
27
+ end
28
+
29
+ def as_json(*args)
30
+ {}.as_json
31
+ end
32
+
33
+ end
34
+
35
+ class ACL < DataType
36
+ # The internal permissions hash and delegate accessors
37
+ attr_accessor :permissions, :delegate
38
+ PUBLIC = "*" # Public priviledges are '*' key in Parse
21
39
 
22
40
  # provide a set of acls and the delegate (for dirty tracking)
23
41
  # { '*' => { "read": true, "write": true } }
@@ -1,3 +1,6 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
1
4
  require_relative '../pointer'
2
5
  require_relative 'collection_proxy'
3
6
  require_relative 'pointer_collection_proxy'
@@ -32,6 +35,11 @@ module Parse
32
35
  opts = {as: key, field: key.to_s.camelize(:lower), required: false}.merge(opts)
33
36
  klassName = opts[:as].to_parse_class
34
37
  parse_field = opts[:field].to_sym
38
+
39
+ ivar = :"@#{key}"
40
+ will_change_method = :"#{key}_will_change!"
41
+ set_attribute_method = :"#{key}_set_attribute!"
42
+
35
43
  if self.fields[key].present? && Parse::Properties::BASE_FIELD_MAP[key].nil?
36
44
  raise Parse::Properties::DefinitionError, "Belongs relation #{self}##{key} already defined with type #{klassName}"
37
45
  end
@@ -56,7 +64,7 @@ module Parse
56
64
 
57
65
  # We generate the getter method
58
66
  define_method(key) do
59
- ivar = :"@#{key}"
67
+
60
68
  val = instance_variable_get ivar
61
69
  # We provide autofetch functionality. If the value is nil and the
62
70
  # current Parse::Object is a pointer, then let's auto fetch it
@@ -68,37 +76,41 @@ module Parse
68
76
  # if for some reason we retrieved either from store or fetching a
69
77
  # hash, lets try to buid a Pointer of that type.
70
78
 
71
- if val.is_a?(Hash) && ( val["__type"].freeze == "Pointer".freeze || val["__type"].freeze == "Object".freeze )
72
- val = Parse::Object.build val, ( val["className"] || klassName )
79
+ if val.is_a?(Hash) && ( val["__type"] == "Pointer" || val["__type"] == "Object" )
80
+ val = Parse::Object.build val, ( val[Parse::Model::KEY_CLASS_NAME] || klassName )
73
81
  instance_variable_set ivar, val
74
82
  end
75
83
  val
76
84
  end
77
85
 
86
+ if self.method_defined?("#{key}?")
87
+ puts "Creating belongs_to helper :#{key}?. Will overwrite existing method #{self}##{key}?."
88
+ end
89
+
78
90
  define_method("#{key}?") do
79
91
  self.send(key).is_a?(Parse::Pointer)
80
92
  end
81
93
 
82
94
  # A proxy setter method that has dirty tracking enabled.
83
95
  define_method("#{key}=") do |val|
84
- send "#{key}_set_attribute!", val, true
96
+ send set_attribute_method, val, true
85
97
  end
86
98
 
87
99
  # We only support pointers, either by object or by transforming a hash.
88
- define_method("#{key}_set_attribute!") do |val, track = true|
100
+ define_method(set_attribute_method) do |val, track = true|
89
101
  if val == Parse::Properties::DELETE_OP
90
102
  val = nil
91
- elsif val.is_a?(Hash) && ( val["__type"].freeze == "Pointer".freeze || val["__type"].freeze == "Object".freeze )
92
- val = Parse::Object.build val, ( val["className"] || klassName )
103
+ elsif val.is_a?(Hash) && ( val["__type"] == "Pointer" || val["__type"] == "Object" )
104
+ val = Parse::Object.build val, ( val[Parse::Model::KEY_CLASS_NAME] || klassName )
93
105
  end
94
106
 
95
107
  if track == true
96
- send :"#{key}_will_change!" unless val == instance_variable_get( :"@#{key}" )
108
+ send will_change_method unless val == instance_variable_get( ivar )
97
109
  end
98
110
 
99
111
  # Never set an object that is not a Parse::Pointer
100
112
  if val.nil? || val.is_a?(Parse::Pointer)
101
- instance_variable_set(:"@#{key}", val)
113
+ instance_variable_set(ivar, val)
102
114
  else
103
115
  warn "[#{self.class}] Invalid value #{val} set for belongs_to field #{key}"
104
116
  end
@@ -110,7 +122,7 @@ module Parse
110
122
  if self.method_defined?(parse_field) == false
111
123
  alias_method parse_field, key
112
124
  alias_method "#{parse_field}=", "#{key}="
113
- alias_method "#{parse_field}_set_attribute!", "#{key}_set_attribute!"
125
+ alias_method "#{parse_field}_set_attribute!", set_attribute_method
114
126
  elsif parse_field.to_sym != :objectId
115
127
  warn "Alias belongs_to method #{self}##{parse_field} already defined."
116
128
  end
@@ -1,3 +1,6 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
1
4
  require 'active_model'
2
5
  require 'active_support'
3
6
  require 'active_support/inflector'
@@ -18,7 +21,7 @@ module Parse
18
21
  class CollectionProxy
19
22
  include ::ActiveModel::Model
20
23
  include ::ActiveModel::Dirty
21
-
24
+ include ::Enumerable
22
25
  attr_accessor :collection, :delegate, :loaded
23
26
  attr_reader :delegate, :key
24
27
  attr_accessor :parse_class
@@ -195,6 +198,16 @@ module Parse
195
198
  collection.each &Proc.new
196
199
  end
197
200
 
201
+ def map
202
+ return collection.enum_for(:map) unless block_given?
203
+ collection.map &Proc.new
204
+ end
205
+
206
+ def select
207
+ return collection.enum_for(:select) unless block_given?
208
+ collection.select &Proc.new
209
+ end
210
+
198
211
  def inspect
199
212
  "#<#{self.class} changed?=#{changed?} @collection=#{@collection.inspect} >"
200
213
  end
@@ -1,3 +1,6 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
1
4
  require_relative '../pointer'
2
5
  require_relative 'collection_proxy'
3
6
  require_relative 'pointer_collection_proxy'
@@ -55,16 +58,75 @@ module Parse
55
58
  # By default, all associations are stored in 'through: :array' form. If you are working with a Parse Relation, you
56
59
  # should specify the 'through: :relation' property instead. This will switch the internal storage mechanisms
57
60
  # from using a PointerCollectionProxy to a RelationCollectionProxy.
61
+ def has_many_queried(key, scope = nil, **opts)
62
+ # key will be the name of the property
63
+ # the remote class is either key or as.
64
+ opts[:scope_only] ||= false
65
+ klassName = (opts[:as] || key).to_parse_class singularize: true
66
+ foreign_field = (opts[:field] || parse_class.columnize ).to_sym
67
+
68
+ define_method(key) do |*args, &block|
69
+ return [] if @id.nil?
70
+ query = Parse::Query.new(klassName, limit: :max)
71
+
72
+ query.where(foreign_field => self) unless opts[:scope_only] == true
73
+
74
+ if scope.is_a?(Proc)
75
+ # magic, override the singleton method_missing with accessing object level methods
76
+ # that don't collide with Parse::Query instance. Still accessible under :i
77
+ instance = self
78
+ query.define_singleton_method(:method_missing) { |m, *args, &block| instance.send(m, *args, &block) }
79
+ query.define_singleton_method(:i) { instance }
80
+ # if the scope takes no arguments, assume arguments are additional conditions
81
+ if scope.arity.zero?
82
+ query.instance_exec(&scope)
83
+ query.conditions(*args) if args.present?
84
+ else
85
+ query.instance_exec(*args,&scope)
86
+ end
87
+ instance = nil # help clean up ruby gc
88
+ elsif args.present?
89
+ query.conditions(*args)
90
+ end
91
+
92
+ query.define_singleton_method(:method_missing) do |m, *args, &block|
93
+ klass = Parse::Model.find_class klassName
94
+ 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
99
+ end
100
+ self.results.send(m, *args, &block)
101
+ end
102
+
103
+ return query if block.nil?
104
+ query.results(&block)
105
+ end
58
106
 
59
- def has_many(key, opts = {})
60
- opts = {through: :array,
107
+ end
108
+
109
+ def has_many(key, scope = nil, **opts)
110
+ opts[:through] ||= :query
111
+
112
+ if opts[:through] == :query
113
+ return has_many_queried(key, scope, opts)
114
+ end
115
+
116
+ # below this is the same
117
+ opts.reverse_merge!({
61
118
  field: key.to_s.camelize(:lower),
62
119
  required: false,
63
- as: key}.merge(opts)
120
+ as: key})
64
121
 
65
122
  klassName = opts[:as].to_parse_class singularize: true
66
- parse_field = opts[:field].to_sym
123
+ parse_field = opts[:field].to_sym # name of the column (local or remote)
67
124
  access_type = opts[:through].to_sym
125
+
126
+ ivar = :"@#{key}"
127
+ will_change_method = :"#{key}_will_change!"
128
+ set_attribute_method = :"#{key}_set_attribute!"
129
+
68
130
  # verify that the user did not duplicate properties or defined different properties with the same name
69
131
  if self.fields[key].present? && Parse::Properties::BASE_FIELD_MAP[key].nil?
70
132
  raise Parse::Properties::DefinitionError, "Has_many property #{self}##{key} already defined with type #{klassName}"
@@ -96,7 +158,6 @@ module Parse
96
158
 
97
159
  # The first method to be defined is a getter.
98
160
  define_method(key) do
99
- ivar = :"@#{key}"
100
161
  val = instance_variable_get(ivar)
101
162
  # if the value for this is nil and we are a pointer, then autofetch
102
163
  if val.nil? && pointer?
@@ -116,11 +177,11 @@ module Parse
116
177
 
117
178
  # proxy setter that forwards with dirty tracking
118
179
  define_method("#{key}=") do |val|
119
- send "#{key}_set_attribute!", val, true
180
+ send set_attribute_method, val, true
120
181
  end
121
182
 
122
183
  # This will set the content of the proxy.
123
- define_method("#{key}_set_attribute!") do |val, track = true|
184
+ define_method(set_attribute_method) do |val, track = true|
124
185
  # If it is a hash, with a __type of Relation, createa a new RelationCollectionProxy, regardless
125
186
  # of what is defined because we must have gotten this from Parse.
126
187
 
@@ -132,14 +193,14 @@ module Parse
132
193
 
133
194
  if val.is_a?(Hash) && val["__type"] == "Relation"
134
195
  relation_objects = val["objects"] || []
135
- val = Parse::RelationCollectionProxy.new relation_objects, delegate: self, key: key, parse_class: (val["className"] || klassName)
196
+ val = Parse::RelationCollectionProxy.new relation_objects, delegate: self, key: key, parse_class: (val[Parse::Model::KEY_CLASS_NAME] || klassName)
136
197
  elsif val.is_a?(Hash) && val["__op"] == "AddRelation" && val["objects"].present?
137
- _collection = proxyKlass.new [], delegate: self, key: key, parse_class: (val["className"] || klassName)
198
+ _collection = proxyKlass.new [], delegate: self, key: key, parse_class: (val[Parse::Model::KEY_CLASS_NAME] || klassName)
138
199
  _collection.loaded = true
139
200
  _collection.add val["objects"].parse_objects
140
201
  val = _collection
141
202
  elsif val.is_a?(Hash) && val["__op"] == "RemoveRelation" && val["objects"].present?
142
- _collection = proxyKlass.new [], delegate: self, key: key, parse_class: (val["className"] || klassName)
203
+ _collection = proxyKlass.new [], delegate: self, key: key, parse_class: (val[Parse::Model::KEY_CLASS_NAME] || klassName)
143
204
  _collection.loaded = true
144
205
  _collection.remove val["objects"].parse_objects
145
206
  val = _collection
@@ -150,11 +211,11 @@ module Parse
150
211
 
151
212
  # send dirty tracking if set
152
213
  if track == true
153
- send :"#{key}_will_change!" unless val == instance_variable_get( :"@#{key}" )
214
+ send will_change_method unless val == instance_variable_get( ivar )
154
215
  end
155
216
  # TODO: Only allow empty proxy collection class as a value or nil.
156
217
  if val.is_a?(Parse::CollectionProxy)
157
- instance_variable_set(:"@#{key}", val)
218
+ instance_variable_set(ivar, val)
158
219
  else
159
220
  warn "[#{self.class}] Invalid value #{val} for :has_many field #{key}. Should be an Array or a CollectionProxy"
160
221
  end
@@ -185,7 +246,7 @@ module Parse
185
246
  if self.method_defined?(parse_field) == false
186
247
  alias_method parse_field, key
187
248
  alias_method "#{parse_field}=", "#{key}="
188
- alias_method "#{parse_field}_set_attribute!", "#{key}_set_attribute!"
249
+ alias_method "#{parse_field}_set_attribute!", set_attribute_method
189
250
  elsif parse_field.to_sym != :objectId
190
251
  warn "Alias has_many method #{self}##{parse_field} already defined."
191
252
  end
@@ -199,14 +260,14 @@ module Parse
199
260
  self.class.relations
200
261
  end
201
262
 
202
- # returns a has of all the relation changes that have been performed on this
263
+ # returns a hash of all the relation changes that have been performed on this
203
264
  # instance.
204
265
  def relation_updates
205
266
  h = {}
206
267
  changed.each do |key|
207
268
  next unless relations[key.to_sym].present? && send(key).changed?
208
269
  remote_field = self.field_map[key.to_sym] || key
209
- h[remote_field] = send key
270
+ h[remote_field] = send key # we still need to send a proxy collection
210
271
  end
211
272
  h
212
273
  end