parse-stack 1.7.3 → 1.9.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (85) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ruby.yml +36 -0
  3. data/.solargraph.yml +23 -0
  4. data/.travis.yml +6 -3
  5. data/Changes.md +84 -22
  6. data/Gemfile +14 -12
  7. data/Gemfile.lock +110 -60
  8. data/README.md +67 -24
  9. data/Rakefile +14 -14
  10. data/bin/parse-console +1 -0
  11. data/lib/parse/api/aggregate.rb +59 -0
  12. data/lib/parse/api/all.rb +2 -1
  13. data/lib/parse/api/analytics.rb +0 -3
  14. data/lib/parse/api/batch.rb +3 -5
  15. data/lib/parse/api/cloud_functions.rb +0 -3
  16. data/lib/parse/api/config.rb +0 -4
  17. data/lib/parse/api/files.rb +3 -7
  18. data/lib/parse/api/hooks.rb +4 -8
  19. data/lib/parse/api/objects.rb +9 -14
  20. data/lib/parse/api/push.rb +0 -4
  21. data/lib/parse/api/schema.rb +2 -6
  22. data/lib/parse/api/server.rb +4 -7
  23. data/lib/parse/api/sessions.rb +2 -5
  24. data/lib/parse/api/users.rb +9 -14
  25. data/lib/parse/client.rb +55 -50
  26. data/lib/parse/client/authentication.rb +29 -33
  27. data/lib/parse/client/batch.rb +8 -11
  28. data/lib/parse/client/body_builder.rb +19 -20
  29. data/lib/parse/client/caching.rb +23 -28
  30. data/lib/parse/client/protocol.rb +11 -12
  31. data/lib/parse/client/request.rb +4 -6
  32. data/lib/parse/client/response.rb +5 -7
  33. data/lib/parse/model/acl.rb +14 -12
  34. data/lib/parse/model/associations/belongs_to.rb +19 -24
  35. data/lib/parse/model/associations/collection_proxy.rb +328 -317
  36. data/lib/parse/model/associations/has_many.rb +22 -27
  37. data/lib/parse/model/associations/has_one.rb +7 -12
  38. data/lib/parse/model/associations/pointer_collection_proxy.rb +5 -13
  39. data/lib/parse/model/associations/relation_collection_proxy.rb +5 -9
  40. data/lib/parse/model/bytes.rb +8 -10
  41. data/lib/parse/model/classes/installation.rb +2 -4
  42. data/lib/parse/model/classes/product.rb +2 -5
  43. data/lib/parse/model/classes/role.rb +3 -5
  44. data/lib/parse/model/classes/session.rb +2 -5
  45. data/lib/parse/model/classes/user.rb +21 -17
  46. data/lib/parse/model/core/actions.rb +31 -46
  47. data/lib/parse/model/core/builder.rb +6 -6
  48. data/lib/parse/model/core/errors.rb +0 -1
  49. data/lib/parse/model/core/fetching.rb +45 -50
  50. data/lib/parse/model/core/properties.rb +53 -68
  51. data/lib/parse/model/core/querying.rb +292 -282
  52. data/lib/parse/model/core/schema.rb +89 -92
  53. data/lib/parse/model/date.rb +16 -23
  54. data/lib/parse/model/file.rb +171 -174
  55. data/lib/parse/model/geopoint.rb +12 -16
  56. data/lib/parse/model/model.rb +31 -37
  57. data/lib/parse/model/object.rb +58 -70
  58. data/lib/parse/model/pointer.rb +177 -176
  59. data/lib/parse/model/push.rb +8 -10
  60. data/lib/parse/model/shortnames.rb +1 -2
  61. data/lib/parse/model/time_zone.rb +3 -5
  62. data/lib/parse/query.rb +70 -37
  63. data/lib/parse/query/constraint.rb +4 -6
  64. data/lib/parse/query/constraints.rb +62 -20
  65. data/lib/parse/query/operation.rb +8 -11
  66. data/lib/parse/query/ordering.rb +45 -49
  67. data/lib/parse/stack.rb +15 -11
  68. data/lib/parse/stack/generators/rails.rb +28 -30
  69. data/lib/parse/stack/generators/templates/model.erb +5 -6
  70. data/lib/parse/stack/generators/templates/model_installation.rb +0 -1
  71. data/lib/parse/stack/generators/templates/model_role.rb +0 -1
  72. data/lib/parse/stack/generators/templates/model_session.rb +0 -1
  73. data/lib/parse/stack/generators/templates/model_user.rb +0 -1
  74. data/lib/parse/stack/generators/templates/parse.rb +9 -9
  75. data/lib/parse/stack/generators/templates/webhooks.rb +1 -2
  76. data/lib/parse/stack/railtie.rb +2 -4
  77. data/lib/parse/stack/tasks.rb +70 -86
  78. data/lib/parse/stack/version.rb +1 -1
  79. data/lib/parse/webhooks.rb +19 -26
  80. data/lib/parse/webhooks/payload.rb +26 -28
  81. data/lib/parse/webhooks/registration.rb +23 -31
  82. data/parse-stack.gemspec +28 -28
  83. data/parse-stack.png +0 -0
  84. metadata +27 -25
  85. data/.github/parse-ruby-sdk.png +0 -0
@@ -1,18 +1,17 @@
1
1
  # encoding: UTF-8
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'active_model'
5
- require 'active_support'
6
- require 'active_support/inflector'
7
- require 'active_support/core_ext'
8
- require 'active_support/core_ext/object'
9
- require 'active_support/inflector'
10
- require 'active_model_serializers'
11
- require 'active_support/inflector'
12
- require 'active_model_serializers'
13
- require 'active_support/hash_with_indifferent_access'
14
- require 'time'
15
-
4
+ require "active_model"
5
+ require "active_support"
6
+ require "active_support/inflector"
7
+ require "active_support/core_ext"
8
+ require "active_support/core_ext/object"
9
+ require "active_support/inflector"
10
+ require "active_model_serializers"
11
+ require "active_support/inflector"
12
+ require "active_model_serializers"
13
+ require "active_support/hash_with_indifferent_access"
14
+ require "time"
16
15
 
17
16
  module Parse
18
17
 
@@ -22,15 +21,15 @@ module Parse
22
21
  # These are the base types supported by Parse.
23
22
  TYPES = [:string, :relation, :integer, :float, :boolean, :date, :array, :file, :geopoint, :bytes, :object, :acl, :timezone].freeze
24
23
  # These are the base mappings of the remote field name types.
25
- BASE = {objectId: :string, createdAt: :date, updatedAt: :date, ACL: :acl}.freeze
24
+ BASE = { objectId: :string, createdAt: :date, updatedAt: :date, ACL: :acl }.freeze
26
25
  # The list of properties that are part of all objects
27
26
  BASE_KEYS = [:id, :created_at, :updated_at].freeze
28
27
  # Default hash map of local attribute name to remote column name
29
- BASE_FIELD_MAP = {id: :objectId, created_at: :createdAt, updated_at: :updatedAt, acl: :ACL}.freeze
28
+ BASE_FIELD_MAP = { id: :objectId, created_at: :createdAt, updated_at: :updatedAt, acl: :ACL }.freeze
30
29
  # The delete operation hash.
31
- CORE_FIELDS = {id: :string, created_at: :date, updated_at: :date, acl: :acl}.freeze
30
+ CORE_FIELDS = { id: :string, created_at: :date, updated_at: :date, acl: :acl }.freeze
32
31
  # The delete operation hash.
33
- DELETE_OP = {"__op"=>"Delete"}.freeze
32
+ DELETE_OP = { "__op" => "Delete" }.freeze
34
33
  # @!visibility private
35
34
  def self.included(base)
36
35
  base.extend(ClassMethods)
@@ -49,7 +48,7 @@ module Parse
49
48
  @fields ||= (self == Parse::Object ? CORE_FIELDS : Parse::Object.fields).dup
50
49
  if type.present?
51
50
  type = type.to_sym
52
- return @fields.select { |k,v| v == type }
51
+ return @fields.select { |k, v| v == type }
53
52
  end
54
53
  @fields
55
54
  end
@@ -101,7 +100,6 @@ module Parse
101
100
  # "myDate" (lower-first-camelcase) with a Parse data type of Date.
102
101
  # You can override the implicit naming behavior by passing the option :field to override.
103
102
  def property(key, data_type = :string, **opts)
104
-
105
103
  key = key.to_sym
106
104
  ivar = :"@#{key}"
107
105
  will_change_method = :"#{key}_will_change!"
@@ -125,25 +123,24 @@ module Parse
125
123
 
126
124
  # set defaults
127
125
  opts = { required: false,
128
- alias: true,
129
- symbolize: false,
130
- enum: nil,
131
- scopes: true,
132
- _prefix: nil,
133
- _suffix: false,
134
- field: key.to_s.camelize(:lower)
135
- }.merge( opts )
126
+ alias: true,
127
+ symbolize: false,
128
+ enum: nil,
129
+ scopes: true,
130
+ _prefix: nil,
131
+ _suffix: false,
132
+ field: key.to_s.camelize(:lower) }.merge(opts)
136
133
  #By default, the remote field name is a lower-first-camelcase version of the key
137
134
  # it can be overriden by the :field parameter
138
135
  parse_field = opts[:field].to_sym
139
136
  # if this is a custom property that is already defined, OR it is a subclass trying to define a core property
140
137
  # then warn and exit.
141
- if (self.fields[key].present? && BASE_FIELD_MAP[key].nil?) || ( self < Parse::Object && BASE_FIELD_MAP.has_key?(key) )
138
+ if (self.fields[key].present? && BASE_FIELD_MAP[key].nil?) || (self < Parse::Object && BASE_FIELD_MAP.has_key?(key))
142
139
  warn "Property #{self}##{key} already defined with data type :#{data_type}. Will be ignored."
143
140
  return false
144
141
  end
145
142
  # We keep the list of fields that are on the remote Parse store
146
- if self.fields[parse_field].present? || ( self < Parse::Object && BASE.has_key?(parse_field) )
143
+ if self.fields[parse_field].present? || (self < Parse::Object && BASE.has_key?(parse_field))
147
144
  warn "Alias property #{self}##{parse_field} conflicts with previously defined property. Will be ignored."
148
145
  return false
149
146
  # raise ArgumentError
@@ -152,14 +149,14 @@ module Parse
152
149
  define_attribute_methods key
153
150
 
154
151
  # this hash keeps list of attributes (based on remote fields) and their data types
155
- self.attributes.merge!( parse_field => data_type )
152
+ self.attributes.merge!(parse_field => data_type)
156
153
  # this maps all the possible attribute fields and their data types. We use both local
157
154
  # keys and remote keys because when we receive a remote object that has the remote field name
158
155
  # we need to know what the data type conversion should be.
159
- self.fields.merge!( key => data_type, parse_field => data_type )
156
+ self.fields.merge!(key => data_type, parse_field => data_type)
160
157
  # This creates a mapping between the local field and the remote field name.
161
- self.field_map.merge!( key => parse_field )
162
- #puts "Current Self: #{self} - #{key} = #{self.attributes}"
158
+ self.field_map.merge!(key => parse_field)
159
+
163
160
  # if the field is marked as required, then add validations
164
161
  if opts[:required]
165
162
  # if integer or float, validate that it's a number
@@ -183,7 +180,6 @@ module Parse
183
180
  is_enum_type = opts[:enum].nil? == false
184
181
 
185
182
  if is_enum_type
186
-
187
183
  unless data_type == :string
188
184
  raise ArgumentError, "Property #{self}##{parse_field} :enum option is only supported on :string data types."
189
185
  end
@@ -196,7 +192,7 @@ module Parse
196
192
 
197
193
  enum_values = enum_values.dup.map(&:to_sym).freeze
198
194
 
199
- self.enums.merge!( key => enum_values )
195
+ self.enums.merge!(key => enum_values)
200
196
  allow_nil = opts[:required] == false
201
197
  validates key, inclusion: { in: enum_values }, allow_nil: allow_nil
202
198
 
@@ -217,9 +213,7 @@ module Parse
217
213
 
218
214
  class_method_name = prefix_or_key.to_s.pluralize.to_sym
219
215
  if singleton_class.method_defined?(class_method_name)
220
- raise ArgumentError, "You tried to define an enum named `#{key}` for #{self} " + \
221
- "but this will generate a method `#{self}.#{class_method_name}` " + \
222
- " which is already defined. Try using :_suffix or :_prefix options."
216
+ raise ArgumentError, "You tried to define an enum named `#{key}` for #{self} " + "but this will generate a method `#{self}.#{class_method_name}` " + " which is already defined. Try using :_suffix or :_prefix options."
223
217
  end
224
218
 
225
219
  define_singleton_method(class_method_name) { enum_values }
@@ -239,18 +233,14 @@ module Parse
239
233
  elsif prefix.present?
240
234
  method_name = :"#{prefix}_#{enum}"
241
235
  end
242
- self.scope method_name, ->(ex = {}){ ex.merge!(key => enum); query( ex ) }
243
-
236
+ self.scope method_name, ->(ex = {}) { ex.merge!(key => enum); query(ex) }
244
237
 
245
238
  define_method("#{method_name}!") { send set_attribute_method, enum, true }
246
239
  define_method("#{method_name}?") { enum == send(key).to_s.to_sym }
247
240
  end
248
241
  end # unless scopes
249
-
250
242
  end # if is enum
251
243
 
252
-
253
-
254
244
  symbolize_value = opts[:symbolize]
255
245
 
256
246
  #only support symbolization of string data types
@@ -271,7 +261,6 @@ module Parse
271
261
  # we'll assume it's just a plain literal value
272
262
  default_value.is_a?(Proc) ? default_value.call(self) : default_value
273
263
  end
274
-
275
264
  end
276
265
 
277
266
  # We define a getter with the key
@@ -295,12 +284,11 @@ module Parse
295
284
  # if value is nil (even after fetching), then lets see if the developer
296
285
  # set a default value for this attribute.
297
286
  if value.nil? && respond_to?("#{key}_default")
298
-
299
- value = send("#{key}_default")
300
- value = format_value(key, value, data_type)
287
+ value = send("#{key}_default")
288
+ value = format_value(key, value, data_type)
301
289
  # lets set the variable with the updated value
302
- instance_variable_set ivar, value
303
- send will_change_method
290
+ instance_variable_set ivar, value
291
+ send will_change_method
304
292
  elsif value.nil? && data_type == :array
305
293
  value = Parse::CollectionProxy.new [], delegate: self, key: key
306
294
  instance_variable_set ivar, value
@@ -318,12 +306,12 @@ module Parse
318
306
  end
319
307
  # finally return the value
320
308
  if symbolize_value
321
- if data_type == :string
322
- return value.respond_to?(:to_sym) ? value.to_sym : value
323
- elsif data_type == :array && value.is_a?(Array)
324
- # value.map(&:to_sym)
325
- return value.compact.map { |m| m.respond_to?(:to_sym) ? m.to_sym : m }
326
- end
309
+ if data_type == :string
310
+ return value.respond_to?(:to_sym) ? value.to_sym : value
311
+ elsif data_type == :array && value.is_a?(Array)
312
+ # value.map(&:to_sym)
313
+ return value.compact.map { |m| m.respond_to?(:to_sym) ? m.to_sym : m }
314
+ end
327
315
  end
328
316
 
329
317
  value
@@ -338,7 +326,7 @@ module Parse
338
326
  # returns true if set to true, false otherwise
339
327
  define_method("#{key}?") { (send(key) == true) }
340
328
  unless opts[:scopes] == false
341
- scope key, ->(opts = {}){ query( opts.merge(key => true) ) }
329
+ scope key, ->(opts = {}) { query(opts.merge(key => true)) }
342
330
  end
343
331
  elsif data_type == :integer || data_type == :float
344
332
  if self.method_defined?("#{key}_increment!")
@@ -359,7 +347,7 @@ module Parse
359
347
  end
360
348
 
361
349
  if self.method_defined?("#{key}_decrement!")
362
- puts "Creating decrement helper :#{key}_decrement!. Will overwrite existing method #{self}##{key}_decrement!."
350
+ warn "Creating decrement helper :#{key}_decrement!. Will overwrite existing method #{self}##{key}_decrement!."
363
351
  end
364
352
 
365
353
  define_method("#{key}_decrement!") do |amount = -1|
@@ -369,7 +357,6 @@ module Parse
369
357
  amount = -amount if amount > 0
370
358
  send("#{key}_increment!", amount)
371
359
  end
372
-
373
360
  end
374
361
 
375
362
  # The second method to be defined is a setter method. This is done by
@@ -393,7 +380,7 @@ module Parse
393
380
  # this will grab the current value and keep a copy of it - but we only do this if
394
381
  # the new value being set is different from the current value stored.
395
382
  if track == true
396
- send will_change_method unless val == instance_variable_get( ivar )
383
+ send will_change_method unless val == instance_variable_get(ivar)
397
384
  end
398
385
 
399
386
  if symbolize_value
@@ -435,7 +422,6 @@ module Parse
435
422
  end
436
423
  true
437
424
  end # property
438
-
439
425
  end #ClassMethods
440
426
 
441
427
  # @return [Hash] a hash mapping of all property fields and their types.
@@ -451,7 +437,7 @@ module Parse
451
437
  # TODO: We can optimize
452
438
  # @return [Hash] returns the list of property attributes for this class.
453
439
  def attributes
454
- {__type: :string, :className => :string}.merge!(self.class.attributes)
440
+ { __type: :string, :className => :string }.merge!(self.class.attributes)
455
441
  end
456
442
 
457
443
  # support for setting a hash of attributes on the object with a given dirty tracking value
@@ -466,7 +452,7 @@ module Parse
466
452
  @id ||= hash[Parse::Model::ID] || hash[Parse::Model::OBJECT_ID] || hash[:objectId]
467
453
  hash.each do |key, value|
468
454
  method = "#{key}_set_attribute!".freeze
469
- send(method, value, dirty_track) if respond_to?( method )
455
+ send(method, value, dirty_track) if respond_to?(method)
470
456
  end
471
457
  end
472
458
 
@@ -494,7 +480,7 @@ module Parse
494
480
  next unless fields[key].present?
495
481
  remote_field = self.field_map[key] || key
496
482
  h[remote_field] = send key
497
- h[remote_field] = {__op: :Delete} if h[remote_field].nil?
483
+ h[remote_field] = { __op: :Delete } if h[remote_field].nil?
498
484
  # in the case that the field is a Parse object, generate a pointer
499
485
  # if it is a Parse::PointerCollectionProxy, then make sure we get a list of pointers.
500
486
  h[remote_field] = h[remote_field].parse_pointers if h[remote_field].is_a?(Parse::PointerCollectionProxy)
@@ -509,6 +495,7 @@ module Parse
509
495
  fields[key.to_sym].present?
510
496
  end
511
497
  end
498
+
512
499
  # Returns a formatted value based on the operation hash and data_type of the property.
513
500
  # For some values in Parse, they are specified as operation hashes which could include
514
501
  # Add, Remove, Delete, AddUnique and Increment.
@@ -585,7 +572,7 @@ module Parse
585
572
  val = val.to_f unless val.blank?
586
573
  when :acl
587
574
  # ACL types go through a special conversion
588
- val = ACL.typecast(val, self)
575
+ val = ACL.typecast(val, self)
589
576
  when :date
590
577
  # if it respond to parse_date, then use that as the conversion.
591
578
  if val.respond_to?(:parse_date) && val.is_a?(Parse::Date) == false
@@ -596,9 +583,9 @@ module Parse
596
583
  elsif val.is_a?(String)
597
584
  # if it's a string, try parsing the date
598
585
  val = Parse::Date.parse val
599
- #elsif val.present?
600
- # pus "[Parse::Stack] Invalid date value '#{val}' assigned to #{self.class}##{key}, it should be a Parse::Date or DateTime."
601
- # raise ValueError, "Invalid date value '#{val}' assigned to #{self.class}##{key}, it should be a Parse::Date or DateTime."
586
+ #elsif val.present?
587
+ # pus "[Parse::Stack] Invalid date value '#{val}' assigned to #{self.class}##{key}, it should be a Parse::Date or DateTime."
588
+ # raise ValueError, "Invalid date value '#{val}' assigned to #{self.class}##{key}, it should be a Parse::Date or DateTime."
602
589
  end
603
590
  when :timezone
604
591
  val = Parse::TimeZone.new(val) if val.present?
@@ -613,7 +600,5 @@ module Parse
613
600
  end
614
601
  val
615
602
  end
616
-
617
603
  end # Properties
618
-
619
604
  end # Parse
@@ -1,320 +1,330 @@
1
1
  # encoding: UTF-8
2
2
  # frozen_string_literal: true
3
3
 
4
- require_relative '../../query'
4
+ require_relative "../../query"
5
5
 
6
6
  module Parse
7
7
  module Core
8
8
  # Defines the querying methods applied to a Parse::Object.
9
9
  module Querying
10
10
 
11
- # This feature is a small subset of the
12
- # {http://guides.rubyonrails.org/active_record_querying.html#scopes
13
- # ActiveRecord named scopes} feature. Scoping allows you to specify
14
- # commonly-used queries which can be referenced as class method calls and
15
- # are chainable with other scopes. You can use every {Parse::Query}
16
- # method previously covered such as `where`, `includes` and `limit`.
17
- #
18
- # class Article < Parse::Object
19
- # property :published, :boolean
20
- # scope :published, -> { query(published: true) }
21
- # end
22
- #
23
- # This is the same as defining your own class method for the query.
24
- #
25
- # class Article < Parse::Object
26
- # def self.published
27
- # query(published: true)
28
- # end
29
- # end
30
- #
31
- # You can also chain scopes and pass parameters. In addition, boolean and
32
- # enumerated properties have automatically generated scopes for you to use.
33
- #
34
- # class Article < Parse::Object
35
- # scope :published, -> { query(published: true) }
36
- #
37
- # property :comment_count, :integer
38
- # property :category
39
- # property :approved, :boolean
40
- #
41
- # scope :published_and_commented, -> { published.where :comment_count.gt => 0 }
42
- # scope :popular_topics, ->(name) { published_and_commented.where category: name }
43
- # end
44
- #
45
- # # simple scope
46
- # Article.published # => where published is true
47
- #
48
- # # chained scope
49
- # Article.published_and_commented # published is true and comment_count > 0
50
- #
51
- # # scope with parameters
52
- # Article.popular_topic("music") # => popular music articles
53
- # # equivalent: where(published: true, :comment_count.gt => 0, category: name)
54
- #
55
- # # automatically generated scope
56
- # Article.approved(category: "tour") # => where approved: true, category: 'tour'
57
- #
58
- # If you would like to turn off automatic scope generation for property types,
59
- # set the option `:scope` to false when declaring the property.
60
- # @param name [Symbol] the name of the scope.
61
- # @param body [Proc] the proc related to the scope.
62
- # @raise ArgumentError if body parameter does not respond to `call`
63
- # @return [Symbol] the name of the singleton method created.
64
- def scope(name, body)
65
- unless body.respond_to?(:call)
66
- raise ArgumentError, 'The scope body needs to be callable.'
67
- end
68
-
69
- name = name.to_sym
70
- if respond_to?(name, true)
71
- puts "Creating scope :#{name}. Will overwrite existing method #{self}.#{name}."
72
- end
11
+ # This feature is a small subset of the
12
+ # {http://guides.rubyonrails.org/active_record_querying.html#scopes
13
+ # ActiveRecord named scopes} feature. Scoping allows you to specify
14
+ # commonly-used queries which can be referenced as class method calls and
15
+ # are chainable with other scopes. You can use every {Parse::Query}
16
+ # method previously covered such as `where`, `includes` and `limit`.
17
+ #
18
+ # class Article < Parse::Object
19
+ # property :published, :boolean
20
+ # scope :published, -> { query(published: true) }
21
+ # end
22
+ #
23
+ # This is the same as defining your own class method for the query.
24
+ #
25
+ # class Article < Parse::Object
26
+ # def self.published
27
+ # query(published: true)
28
+ # end
29
+ # end
30
+ #
31
+ # You can also chain scopes and pass parameters. In addition, boolean and
32
+ # enumerated properties have automatically generated scopes for you to use.
33
+ #
34
+ # class Article < Parse::Object
35
+ # scope :published, -> { query(published: true) }
36
+ #
37
+ # property :comment_count, :integer
38
+ # property :category
39
+ # property :approved, :boolean
40
+ #
41
+ # scope :published_and_commented, -> { published.where :comment_count.gt => 0 }
42
+ # scope :popular_topics, ->(name) { published_and_commented.where category: name }
43
+ # end
44
+ #
45
+ # # simple scope
46
+ # Article.published # => where published is true
47
+ #
48
+ # # chained scope
49
+ # Article.published_and_commented # published is true and comment_count > 0
50
+ #
51
+ # # scope with parameters
52
+ # Article.popular_topic("music") # => popular music articles
53
+ # # equivalent: where(published: true, :comment_count.gt => 0, category: name)
54
+ #
55
+ # # automatically generated scope
56
+ # Article.approved(category: "tour") # => where approved: true, category: 'tour'
57
+ #
58
+ # If you would like to turn off automatic scope generation for property types,
59
+ # set the option `:scope` to false when declaring the property.
60
+ # @param name [Symbol] the name of the scope.
61
+ # @param body [Proc] the proc related to the scope.
62
+ # @raise ArgumentError if body parameter does not respond to `call`
63
+ # @return [Symbol] the name of the singleton method created.
64
+ def scope(name, body)
65
+ unless body.respond_to?(:call)
66
+ raise ArgumentError, "The scope body needs to be callable."
67
+ end
73
68
 
74
- define_singleton_method(name) do |*args, &block|
69
+ name = name.to_sym
70
+ if respond_to?(name, true)
71
+ puts "Creating scope :#{name}. Will overwrite existing method #{self}.#{name}."
72
+ end
75
73
 
76
- if body.arity.zero?
77
- res = body.call
78
- res.conditions(*args) if args.present?
79
- else
80
- res = body.call(*args)
81
- end
74
+ define_singleton_method(name) do |*args, &block|
75
+ if body.arity.zero?
76
+ res = body.call
77
+ res.conditions(*args) if args.present?
78
+ else
79
+ res = body.call(*args)
80
+ end
82
81
 
83
- _q = res || query
82
+ _q = res || query
84
83
 
85
- if _q.is_a?(Parse::Query)
86
- klass = self
87
- _q.define_singleton_method(:method_missing) do |m, *args, &chained_block|
88
- if klass.respond_to?(m, true)
89
- # must be a scope
90
- klass_scope = klass.send(m, *args)
91
- if klass_scope.is_a?(Parse::Query)
92
- # merge constraints
93
- add_constraints( klass_scope.constraints )
94
- # if a block was passed, execute the query, otherwise return the query
95
- return chained_block.present? ? results(&chained_block) : self
96
- end # if
97
- klass = nil # help clean up ruby gc
98
- return klass_scope
99
- end
84
+ if _q.is_a?(Parse::Query)
85
+ klass = self
86
+ _q.define_singleton_method(:method_missing) do |m, *args, &chained_block|
87
+ if klass.respond_to?(m, true)
88
+ # must be a scope
89
+ klass_scope = klass.send(m, *args)
90
+ if klass_scope.is_a?(Parse::Query)
91
+ # merge constraints
92
+ add_constraints(klass_scope.constraints)
93
+ # if a block was passed, execute the query, otherwise return the query
94
+ return chained_block.present? ? results(&chained_block) : self
95
+ end # if
100
96
  klass = nil # help clean up ruby gc
101
- return results.send(m, *args, &chained_block)
97
+ return klass_scope
102
98
  end
99
+ klass = nil # help clean up ruby gc
100
+ return results.send(m, *args, &chained_block)
103
101
  end
104
-
105
- Parse::Query.apply_auto_introspection!(_q)
106
-
107
- return _q if block.nil?
108
- _q.results(&block)
109
102
  end
110
103
 
111
- end
104
+ Parse::Query.apply_auto_introspection!(_q)
112
105
 
113
-
114
- # Creates a new {Parse::Query} with the given constraints for this class.
115
- # @example
116
- # # assume Post < Parse::Object
117
- # query = Post.query(:updated_at.before => DateTime.now)
118
- # @return [Parse::Query] a new query with the given constraints for this
119
- # Parse::Object subclass.
120
- def query(constraints = {})
121
- Parse::Query.new self.parse_class, constraints
122
- end; alias_method :where, :query
123
-
124
- # @param conditions (see Parse::Query#where)
125
- # @return (see Parse::Query#where)
126
- # @see Parse::Query#where
127
- def literal_where(conditions = {})
128
- query.where(conditions)
106
+ return _q if block.nil?
107
+ _q.results(&block)
129
108
  end
109
+ end
130
110
 
131
- # This methods allow you to efficiently iterate over all the records in the collection
132
- # (lower memory cost) at a minor cost of performance. This method utilizes
133
- # the `created_at` field of Parse records to order and iterate over *all* matching records,
134
- # therefore you should not use this method if you want to perform a query
135
- # with constraints against the `created_at` field or need specific type of ordering.
136
- # If you need to use `:created_at` in your constraints, consider using {Parse::Core::Querying#all} or
137
- # {Parse::Core::Actions::ClassMethods#save_all}
138
- # @param constraints [Hash] a set of query constraints.
139
- # @yield a block which will iterate through each matching record.
140
- # @example
141
- #
142
- # post = Post.first
143
- # # iterate over all comments matching conditions
144
- # Comment.each(post: post) do |comment|
145
- # # ...
146
- # end
147
- # @return [Parse::Object] the last Parse::Object record processed.
148
- # @note You cannot use *:created_at* as a constraint.
149
- # @raise ArgumentError if :created_at is detected in the constraints argument.
150
- # @see Parse::Core::Querying.all
151
- # @see Parse::Core::Actions.save_all
152
- def each(constraints = {}, &block)
153
- # verify we don't hvae created at as a constraint, otherwise this will not work
154
- invalid_constraints = constraints.keys.any? do |k|
155
- (k == :created_at || k == :createdAt) ||
156
- ( k.is_a?(Parse::Operation) && (k.operand == :created_at || k.operand == :createdAt) )
157
- end
158
- if invalid_constraints
159
- raise ArgumentError, "[#{self.class}.each] Special method each()" \
160
- "cannot be used with a :created_at constraint."
161
- end
162
- batch_size = 250
163
- start_cursor = first( order: :created_at.asc, keys: :created_at )
164
- constraints.merge! cache: false, limit: batch_size, order: :created_at.asc
165
- all_query = query(constraints)
166
- cursor = start_cursor
167
- # the exclusion set is a set of ids not to include the next query.
168
- exclusion_set = []
169
- loop do
170
- _q = query(constraints.dup)
171
- _q.where(:created_at.on_or_after => cursor.created_at)
172
- # set of ids not to include in the next query. non-performant, but accurate.
173
- _q.where(:id.nin => exclusion_set) unless exclusion_set.empty?
174
- results = _q.results # get results
111
+ # Creates a new {Parse::Query} with the given constraints for this class.
112
+ # @example
113
+ # # assume Post < Parse::Object
114
+ # query = Post.query(:updated_at.before => DateTime.now)
115
+ # @return [Parse::Query] a new query with the given constraints for this
116
+ # Parse::Object subclass.
117
+ def query(constraints = {})
118
+ Parse::Query.new self.parse_class, constraints
119
+ end;
120
+ alias_method :where, :query
175
121
 
176
- break cursor if results.empty? # break if no results
177
- results.each(&block)
178
- next_cursor = results.last
179
- # break if we got less than the maximum requested
180
- break next_cursor if results.count < batch_size
181
- # break if the next object is the same as the current object.
182
- break next_cursor if cursor.id == next_cursor.id
183
- # The exclusion set is used in the case where multiple records have the exact
184
- # same created_at date (down to the microsecond). This prevents getting the same
185
- # record in the next query request.
186
- exclusion_set = results.select { |r| r.created_at == next_cursor.created_at }.map(&:id)
187
- results = nil
188
- cursor = next_cursor
189
- end
122
+ # @param conditions (see Parse::Query#where)
123
+ # @return (see Parse::Query#where)
124
+ # @see Parse::Query#where
125
+ def literal_where(conditions = {})
126
+ query.where(conditions)
127
+ end
190
128
 
129
+ # This methods allow you to efficiently iterate over all the records in the collection
130
+ # (lower memory cost) at a minor cost of performance. This method utilizes
131
+ # the `created_at` field of Parse records to order and iterate over *all* matching records,
132
+ # therefore you should not use this method if you want to perform a query
133
+ # with constraints against the `created_at` field or need specific type of ordering.
134
+ # If you need to use `:created_at` in your constraints, consider using {Parse::Core::Querying#all} or
135
+ # {Parse::Core::Actions::ClassMethods#save_all}
136
+ # @param constraints [Hash] a set of query constraints.
137
+ # @yield a block which will iterate through each matching record.
138
+ # @example
139
+ #
140
+ # post = Post.first
141
+ # # iterate over all comments matching conditions
142
+ # Comment.each(post: post) do |comment|
143
+ # # ...
144
+ # end
145
+ # @return [Parse::Object] the last Parse::Object record processed.
146
+ # @note You cannot use *:created_at* as a constraint.
147
+ # @raise ArgumentError if :created_at is detected in the constraints argument.
148
+ # @see Parse::Core::Querying.all
149
+ # @see Parse::Core::Actions.save_all
150
+ def each(constraints = {}, &block)
151
+ # verify we don't hvae created at as a constraint, otherwise this will not work
152
+ invalid_constraints = constraints.keys.any? do |k|
153
+ (k == :created_at || k == :createdAt) ||
154
+ (k.is_a?(Parse::Operation) && (k.operand == :created_at || k.operand == :createdAt))
191
155
  end
192
-
193
- # Fetch all matching objects in this collection matching the constraints.
194
- # This will be the most common way when querying Parse objects for a subclass.
195
- # When no block is passed, all objects are returned. Using a block is more memory
196
- # efficient as matching objects are fetched in batches and discarded after the iteration
197
- # is completed.
198
- # @param constraints [Hash] a set of {Parse::Query} constraints.
199
- # @yield a block to iterate with each matching object.
200
- # @example
201
- #
202
- # songs = Song.all( ... expressions ...) # => array of Parse::Objects
203
- # # memory efficient for large amounts of records.
204
- # Song.all( ... expressions ...) do |song|
205
- # # ... do something with song..
206
- # end
207
- #
208
- # @note This method will continually query for records by automatically
209
- # incrementing the *:skip* parameter until no more results are returned
210
- # by the server.
211
- # @return [Array<Parse::Object>] an array of matching objects. If a block is passed,
212
- # an empty array is returned.
213
- def all(constraints = {limit: :max})
214
- constraints = constraints.reverse_merge({limit: :max})
215
- prepared_query = query(constraints)
216
- return prepared_query.results(&Proc.new) if block_given?
217
- prepared_query.results
156
+ if invalid_constraints
157
+ raise ArgumentError, "[#{self.class}.each] Special method each()" \
158
+ "cannot be used with a :created_at constraint."
218
159
  end
160
+ batch_size = 250
161
+ start_cursor = first(order: :created_at.asc, keys: :created_at)
162
+ constraints.merge! cache: false, limit: batch_size, order: :created_at.asc
163
+ all_query = query(constraints)
164
+ cursor = start_cursor
165
+ # the exclusion set is a set of ids not to include the next query.
166
+ exclusion_set = []
167
+ loop do
168
+ _q = query(constraints.dup)
169
+ _q.where(:created_at.on_or_after => cursor.created_at)
170
+ # set of ids not to include in the next query. non-performant, but accurate.
171
+ _q.where(:id.nin => exclusion_set) unless exclusion_set.empty?
172
+ results = _q.results # get results
219
173
 
220
- # Returns the first item matching the constraint.
221
- # @overload first(count = 1)
222
- # @param count [Interger] The number of items to return.
223
- # @example
224
- # Object.first(2) # => an array of the first 2 objects in the collection.
225
- # @return [Parse::Object] if count == 1
226
- # @return [Array<Parse::Object>] if count > 1
227
- # @overload first(constraints = {})
228
- # @param constraints [Hash] a set of {Parse::Query} constraints.
229
- # @example
230
- # Object.first( :name => "Anthony" )
231
- # @return [Parse::Object] the first matching object.
232
- def first(constraints = {})
233
- fetch_count = 1
234
- if constraints.is_a?(Numeric)
235
- fetch_count = constraints.to_i
236
- constraints = {}
237
- end
238
- constraints.merge!( {limit: fetch_count} )
239
- res = query(constraints).results
240
- return res.first if fetch_count == 1
241
- return res.first fetch_count
174
+ break cursor if results.empty? # break if no results
175
+ results.each(&block)
176
+ next_cursor = results.last
177
+ # break if we got less than the maximum requested
178
+ break next_cursor if results.count < batch_size
179
+ # break if the next object is the same as the current object.
180
+ break next_cursor if cursor.id == next_cursor.id
181
+ # The exclusion set is used in the case where multiple records have the exact
182
+ # same created_at date (down to the microsecond). This prevents getting the same
183
+ # record in the next query request.
184
+ exclusion_set = results.select { |r| r.created_at == next_cursor.created_at }.map(&:id)
185
+ results = nil
186
+ cursor = next_cursor
242
187
  end
188
+ end
243
189
 
244
- # Creates a count request which is more performant when counting objects.
245
- # @example
246
- # # number of songs with a like count greater than 20.
247
- # count = Song.count( :like_count.gt => 20 )
248
- # @param constraints (see #all)
249
- # @return [Interger] the number of records matching the query.
250
- # @see Parse::Query#count
251
- def count(constraints = {})
252
- query(constraints).count
253
- end
190
+ # Fetch all matching objects in this collection matching the constraints.
191
+ # This will be the most common way when querying Parse objects for a subclass.
192
+ # When no block is passed, all objects are returned. Using a block is more memory
193
+ # efficient as matching objects are fetched in batches and discarded after the iteration
194
+ # is completed.
195
+ # @param constraints [Hash] a set of {Parse::Query} constraints.
196
+ # @yield a block to iterate with each matching object.
197
+ # @example
198
+ #
199
+ # songs = Song.all( ... expressions ...) # => array of Parse::Objects
200
+ # # memory efficient for large amounts of records.
201
+ # Song.all( ... expressions ...) do |song|
202
+ # # ... do something with song..
203
+ # end
204
+ #
205
+ # @note This method will continually query for records by automatically
206
+ # incrementing the *:skip* parameter until no more results are returned
207
+ # by the server.
208
+ # @return [Array<Parse::Object>] an array of matching objects. If a block is passed,
209
+ # an empty array is returned.
210
+ def all(constraints = { limit: :max })
211
+ constraints = constraints.reverse_merge({ limit: :max })
212
+ prepared_query = query(constraints)
213
+ return prepared_query.results(&Proc.new) if block_given?
214
+ prepared_query.results
215
+ end
254
216
 
255
- # Find objects matching the constraint ordered by the descending created_at date.
256
- # @param constraints (see #all)
257
- # @return [Array<Parse::Object>]
258
- def newest(constraints = {})
259
- constraints.merge!(order: :created_at.desc)
260
- _q = query(constraints)
261
- _q.define_singleton_method(:method_missing) { |m, *args, &block| self.results.send(m, *args, &block) }
262
- _q
217
+ # Returns the first item matching the constraint.
218
+ # @overload first(count = 1)
219
+ # @param count [Interger] The number of items to return.
220
+ # @example
221
+ # Object.first(2) # => an array of the first 2 objects in the collection.
222
+ # @return [Parse::Object] if count == 1
223
+ # @return [Array<Parse::Object>] if count > 1
224
+ # @overload first(constraints = {})
225
+ # @param constraints [Hash] a set of {Parse::Query} constraints.
226
+ # @example
227
+ # Object.first( :name => "Anthony" )
228
+ # @return [Parse::Object] the first matching object.
229
+ def first(constraints = {})
230
+ fetch_count = 1
231
+ if constraints.is_a?(Numeric)
232
+ fetch_count = constraints.to_i
233
+ constraints = {}
263
234
  end
235
+ constraints.merge!({ limit: fetch_count })
236
+ res = query(constraints).results
237
+ return res.first if fetch_count == 1
238
+ return res.first fetch_count
239
+ end
264
240
 
265
- # Find objects matching the constraint ordered by the ascending created_at date.
266
- # @param constraints (see #all)
267
- # @return [Array<Parse::Object>]
268
- def oldest(constraints = {})
269
- constraints.merge!(order: :created_at.asc)
270
- _q = query(constraints)
271
- _q.define_singleton_method(:method_missing) { |m, *args, &block| self.results.send(m, *args, &block) }
272
- _q
273
- end
241
+ # Creates a count request which is more performant when counting objects.
242
+ # @example
243
+ # # number of songs with a like count greater than 20.
244
+ # count = Song.count( :like_count.gt => 20 )
245
+ # @param constraints (see #all)
246
+ # @return [Interger] the number of records matching the query.
247
+ # @see Parse::Query#count
248
+ def count(constraints = {})
249
+ query(constraints).count
250
+ end
274
251
 
275
- # Find objects for a given objectId in this collection.The result is a list
276
- # (or single item) of the objects that were successfully found.
277
- # @example
278
- # Object.find "<objectId>"
279
- # Object.find "<objectId>", "<objectId>"....
280
- # Object.find ["<objectId>", "<objectId>"]
281
- # @param parse_ids [String] the objectId to find.
282
- # @param type [Symbol] the fetching methodology to use if more than one id was passed.
283
- # - *:parallel* : Utilizes parrallel HTTP requests to fetch all objects requested.
284
- # - *:batch* : This uses a batch fetch request using a contained_in clause.
285
- # @param compact [Boolean] whether to remove nil items from the returned array for objects
286
- # that were not found.
287
- # @return [Parse::Object] if only one id was provided as a parameter.
288
- # @return [Array<Parse::Object>] if more than one id was provided as a parameter.
289
- def find(*parse_ids, type: :parallel, compact: true)
290
- # flatten the list of Object ids.
291
- parse_ids.flatten!
292
- parse_ids.compact!
293
- # determines if the result back to the call site is an array or a single result
294
- as_array = parse_ids.count > 1
295
- results = []
252
+ # Finds the distinct values for a specified field across a single
253
+ # collection or view and returns the results in an array.
254
+ # @example
255
+ # # get a list of unique city names for users who are older than 21.
256
+ # cities = User.distinct(:city, :age.gt => 21 )
257
+ # @param field The name of the field to use for unique aggregation.
258
+ # @param constraints (see #all)
259
+ # @return [Array] a list of distinct values
260
+ # @see Parse::Query#distinct
261
+ def distinct(field, constraints = {})
262
+ query(constraints).distinct(field)
263
+ end
296
264
 
297
- if type == :batch
298
- # use a .in query with the given id as a list
299
- results = self.class.all(:id.in => parse_ids)
300
- else
301
- # use Parallel to make multiple threaded requests for finding these objects.
302
- # The benefit of using this as default is that each request goes to a specific URL
303
- # which is better than Query request (table scan). This in turn allows for caching of
304
- # individual objects.
305
- results = parse_ids.threaded_map do |parse_id|
306
- next nil unless parse_id.present?
307
- response = client.fetch_object(parse_class, parse_id)
308
- next nil if response.error?
309
- Parse::Object.build response.result, parse_class
310
- end
311
- end
312
- # removes any nil items in the array
313
- results.compact! if compact
265
+ # Find objects matching the constraint ordered by the descending created_at date.
266
+ # @param constraints (see #all)
267
+ # @return [Array<Parse::Object>]
268
+ def newest(constraints = {})
269
+ constraints.merge!(order: :created_at.desc)
270
+ _q = query(constraints)
271
+ _q.define_singleton_method(:method_missing) { |m, *args, &block| self.results.send(m, *args, &block) }
272
+ _q
273
+ end
274
+
275
+ # Find objects matching the constraint ordered by the ascending created_at date.
276
+ # @param constraints (see #all)
277
+ # @return [Array<Parse::Object>]
278
+ def oldest(constraints = {})
279
+ constraints.merge!(order: :created_at.asc)
280
+ _q = query(constraints)
281
+ _q.define_singleton_method(:method_missing) { |m, *args, &block| self.results.send(m, *args, &block) }
282
+ _q
283
+ end
314
284
 
315
- as_array ? results : results.first
316
- end; alias_method :get, :find
285
+ # Find objects for a given objectId in this collection.The result is a list
286
+ # (or single item) of the objects that were successfully found.
287
+ # @example
288
+ # Object.find "<objectId>"
289
+ # Object.find "<objectId>", "<objectId>"....
290
+ # Object.find ["<objectId>", "<objectId>"]
291
+ # @param parse_ids [String] the objectId to find.
292
+ # @param type [Symbol] the fetching methodology to use if more than one id was passed.
293
+ # - *:parallel* : Utilizes parrallel HTTP requests to fetch all objects requested.
294
+ # - *:batch* : This uses a batch fetch request using a contained_in clause.
295
+ # @param compact [Boolean] whether to remove nil items from the returned array for objects
296
+ # that were not found.
297
+ # @return [Parse::Object] if only one id was provided as a parameter.
298
+ # @return [Array<Parse::Object>] if more than one id was provided as a parameter.
299
+ def find(*parse_ids, type: :parallel, compact: true)
300
+ # flatten the list of Object ids.
301
+ parse_ids.flatten!
302
+ parse_ids.compact!
303
+ # determines if the result back to the call site is an array or a single result
304
+ as_array = parse_ids.count > 1
305
+ results = []
306
+
307
+ if type == :batch
308
+ # use a .in query with the given id as a list
309
+ results = self.class.all(:id.in => parse_ids)
310
+ else
311
+ # use Parallel to make multiple threaded requests for finding these objects.
312
+ # The benefit of using this as default is that each request goes to a specific URL
313
+ # which is better than Query request (table scan). This in turn allows for caching of
314
+ # individual objects.
315
+ results = parse_ids.threaded_map do |parse_id|
316
+ next nil unless parse_id.present?
317
+ response = client.fetch_object(parse_class, parse_id)
318
+ next nil if response.error?
319
+ Parse::Object.build response.result, parse_class
320
+ end
321
+ end
322
+ # removes any nil items in the array
323
+ results.compact! if compact
317
324
 
325
+ as_array ? results : results.first
326
+ end;
327
+ alias_method :get, :find
318
328
  end # Querying
319
329
  end
320
330
  end