parse-stack 1.7.3 → 1.9.1

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 (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