parse-stack 1.5.3 → 1.6.0

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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/.github/parse-ruby-sdk.png +0 -0
  3. data/Changes.md +25 -1
  4. data/Gemfile.lock +4 -4
  5. data/README.md +37 -31
  6. data/bin/console +3 -0
  7. data/lib/parse/api/all.rb +2 -1
  8. data/lib/parse/api/apps.rb +12 -0
  9. data/lib/parse/api/config.rb +5 -1
  10. data/lib/parse/api/files.rb +1 -0
  11. data/lib/parse/api/hooks.rb +1 -0
  12. data/lib/parse/api/objects.rb +4 -1
  13. data/lib/parse/api/push.rb +1 -0
  14. data/lib/parse/api/{schemas.rb → schema.rb} +7 -0
  15. data/lib/parse/api/server.rb +44 -0
  16. data/lib/parse/api/sessions.rb +1 -0
  17. data/lib/parse/api/users.rb +4 -1
  18. data/lib/parse/client.rb +109 -73
  19. data/lib/parse/client/authentication.rb +2 -1
  20. data/lib/parse/client/batch.rb +9 -1
  21. data/lib/parse/client/body_builder.rb +16 -1
  22. data/lib/parse/client/caching.rb +15 -13
  23. data/lib/parse/client/protocol.rb +27 -15
  24. data/lib/parse/client/response.rb +26 -8
  25. data/lib/parse/model/acl.rb +1 -1
  26. data/lib/parse/model/associations/belongs_to.rb +18 -19
  27. data/lib/parse/model/associations/collection_proxy.rb +6 -0
  28. data/lib/parse/model/associations/has_many.rb +5 -6
  29. data/lib/parse/model/bytes.rb +4 -1
  30. data/lib/parse/model/classes/user.rb +46 -44
  31. data/lib/parse/model/core/actions.rb +508 -460
  32. data/lib/parse/model/core/builder.rb +75 -0
  33. data/lib/parse/model/core/errors.rb +9 -0
  34. data/lib/parse/model/core/fetching.rb +42 -38
  35. data/lib/parse/model/core/properties.rb +46 -27
  36. data/lib/parse/model/core/querying.rb +231 -228
  37. data/lib/parse/model/core/schema.rb +76 -74
  38. data/lib/parse/model/date.rb +10 -2
  39. data/lib/parse/model/file.rb +16 -2
  40. data/lib/parse/model/geopoint.rb +9 -2
  41. data/lib/parse/model/model.rb +38 -7
  42. data/lib/parse/model/object.rb +60 -19
  43. data/lib/parse/model/pointer.rb +22 -1
  44. data/lib/parse/model/push.rb +6 -2
  45. data/lib/parse/query.rb +57 -11
  46. data/lib/parse/query/constraint.rb +5 -2
  47. data/lib/parse/query/constraints.rb +588 -589
  48. data/lib/parse/query/ordering.rb +2 -2
  49. data/lib/parse/stack.rb +1 -0
  50. data/lib/parse/stack/version.rb +1 -1
  51. data/lib/parse/webhooks.rb +30 -29
  52. data/lib/parse/webhooks/payload.rb +181 -168
  53. data/lib/parse/webhooks/registration.rb +1 -1
  54. data/parse-stack.gemspec +9 -9
  55. metadata +9 -12
@@ -0,0 +1,75 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ require 'active_support'
5
+ require 'active_support/inflector'
6
+ require 'active_support/core_ext'
7
+ require_relative '../object'
8
+
9
+ module Parse
10
+ # Create all Parse::Object subclasses, including their properties and inferred
11
+ # associations by importing the schema for the remote collections in a Parse
12
+ # application. Uses the default configured client.
13
+ # @return [Array] an array of created Parse::Object subclasses.
14
+ # @see Parse::Model::Builder.build!
15
+ def self.auto_generate_models!
16
+ Parse.schemas.map do |schema|
17
+ Parse::Model::Builder.build!(schema)
18
+ end
19
+ end
20
+
21
+ class Model
22
+ # This class provides a method to automatically generate Parse::Object subclasses, including
23
+ # their properties and inferred associations by importing the schema for the remote collections
24
+ # in a Parse application.
25
+ class Builder
26
+
27
+ # Builds a ruby Parse::Object subclass with the provided schema information.
28
+ # @param schema [Hash] the Parse-formatted hash schema for a collection. This hash
29
+ # should two keys:
30
+ # * className: Contains the name of the collection.
31
+ # * field: A hash containg the column fields and their type.
32
+ # @raise ArgumentError when the className could not be inferred from the schema.
33
+ # @return [Array] an array of Parse::Object subclass constants.
34
+ def self.build!(schema)
35
+ unless schema.is_a?(Hash)
36
+ raise ArgumentError, "Schema parameter should be a Parse schema hash object."
37
+ end
38
+ schema = schema.with_indifferent_access
39
+ fields = schema[:fields] || {}
40
+ className = schema[:className]
41
+
42
+ if className.blank?
43
+ raise ArgumentError, "No valid className provided for schema hash"
44
+ end
45
+
46
+ begin
47
+ klass = Parse::Model.find_class className
48
+ klass = ::Object.const_get(className.to_parse_class) if klass.nil?
49
+ rescue => e
50
+ klass = ::Class.new(Parse::Object)
51
+ ::Object.const_set(className, klass)
52
+ end
53
+
54
+ base_fields = Parse::Properties::BASE.keys
55
+ class_fields = klass.field_map.values + [:className]
56
+ fields.each do |field, type|
57
+ field = field.to_sym
58
+ key = field.to_s.underscore.to_sym
59
+ next if base_fields.include?(field) || class_fields.include?(field)
60
+
61
+ data_type = type[:type].downcase.to_sym
62
+ if data_type == :pointer
63
+ klass.belongs_to key, as: type[:targetClass], field: field
64
+ elsif data_type == :relation
65
+ klass.has_many key, as: type[:targetClass], field: field
66
+ else
67
+ klass.property key, data_type, field: field
68
+ end
69
+ class_fields.push(field)
70
+ end
71
+ klass
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,9 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ # The set of all Parse errors.
5
+ module Parse
6
+ # An abstract parent class for all Parse::Error types.
7
+ class Error < StandardError; end
8
+
9
+ end
@@ -1,54 +1,58 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
1
3
 
2
4
  require 'time'
3
5
  require 'parallel'
4
6
 
5
7
  module Parse
6
- # Defines the record fetching interface for instances of Parse::Object.
7
- module Fetching
8
+ # Combines a set of core functionality for {Parse::Object} and its subclasses.
9
+ module Core
10
+ # Defines the record fetching interface for instances of Parse::Object.
11
+ module Fetching
12
+
13
+ # Force fetches and updates the current object with the data contained in the Parse collection.
14
+ # The changes applied to the object are not dirty tracked.
15
+ # @param opts [Hash] a set of options to pass to the client request.
16
+ # @return [self] the current object, useful for chaining.
17
+ def fetch!(opts = {})
18
+ response = client.fetch_object(parse_class, id, opts)
19
+ if response.error?
20
+ puts "[Fetch Error] #{response.code}: #{response.error}"
21
+ end
22
+ # take the result hash and apply it to the attributes.
23
+ apply_attributes!(response.result, dirty_track: false)
24
+ clear_changes!
25
+ self
26
+ end
8
27
 
9
- # Force fetches and updates the current object with the data contained in the Parse collection.
10
- # The changes applied to the object are not dirty tracked.
11
- # @param opts [Hash] a set of options to pass to the client request.
12
- # @return [self] the current object, useful for chaining.
13
- def fetch!(opts = {})
14
- response = client.fetch_object(parse_class, id, opts)
15
- if response.error?
16
- puts "[Fetch Error] #{response.code}: #{response.error}"
28
+ # Fetches the object from the Parse data store if the object is in a Pointer
29
+ # state. This is similar to the `fetchIfNeeded` action in the standard Parse client SDK.
30
+ # @return [self] the current object.
31
+ def fetch
32
+ # if it is a pointer, then let's go fetch the rest of the content
33
+ pointer? ? fetch! : self
17
34
  end
18
- # take the result hash and apply it to the attributes.
19
- apply_attributes!(response.result, dirty_track: false)
20
- clear_changes!
21
- self
22
- end
23
35
 
24
- # Fetches the object from the Parse data store if the object is in a Pointer
25
- # state. This is similar to the `fetchIfNeeded` action in the standard Parse client SDK.
26
- # @return [self] the current object.
27
- def fetch
28
- # if it is a pointer, then let's go fetch the rest of the content
29
- pointer? ? fetch! : self
30
- end
36
+ # Autofetches the object based on a key that is not part {Parse::Properties::BASE_KEYS}.
37
+ # If the key is not a Parse standard key, and the current object is in a
38
+ # Pointer state, then fetch the data related to this record from the Parse
39
+ # data store.
40
+ # @param key [String] the name of the attribute being accessed.
41
+ # @return [Boolean]
42
+ def autofetch!(key)
43
+ key = key.to_sym
44
+ @fetch_lock ||= false
45
+ if @fetch_lock != true && pointer? && key != :acl && Parse::Properties::BASE_KEYS.include?(key) == false && respond_to?(:fetch)
46
+ #puts "AutoFetching Triggerd by: #{self.class}.#{key} (#{id})"
47
+ @fetch_lock = true
48
+ send :fetch
49
+ @fetch_lock = false
50
+ end
31
51
 
32
- # Autofetches the object based on a key that is not part {Parse::Properties::BASE_KEYS}.
33
- # If the key is not a Parse standard key, and the current object is in a
34
- # Pointer state, then fetch the data related to this record from the Parse
35
- # data store.
36
- # @param key [String] the name of the attribute being accessed.
37
- # @return [Boolean]
38
- def autofetch!(key)
39
- key = key.to_sym
40
- @fetch_lock ||= false
41
- if @fetch_lock != true && pointer? && key != :acl && Parse::Properties::BASE_KEYS.include?(key) == false && respond_to?(:fetch)
42
- #puts "AutoFetching Triggerd by: #{self.class}.#{key} (#{id})"
43
- @fetch_lock = true
44
- send :fetch
45
- @fetch_lock = false
46
52
  end
47
53
 
48
54
  end
49
-
50
55
  end
51
-
52
56
  end
53
57
 
54
58
 
@@ -19,10 +19,6 @@ module Parse
19
19
  # This module provides support for handling all the different types of column data types
20
20
  # supported in Parse and mapping them between their remote names with their local ruby named attributes.
21
21
  module Properties
22
- # This is an exception that is thrown if there is an issue when creating a specific property for a class.
23
- class DefinitionError < StandardError; end;
24
- class ValueError < StandardError; end;
25
-
26
22
  # These are the base types supported by Parse.
27
23
  TYPES = [:id, :string, :relation, :integer, :float, :boolean, :date, :array, :file, :geopoint, :bytes, :object, :acl].freeze
28
24
  # These are the base mappings of the remote field name types.
@@ -31,13 +27,15 @@ module Parse
31
27
  BASE_KEYS = [:id, :created_at, :updated_at].freeze
32
28
  # Default hash map of local attribute name to remote column name
33
29
  BASE_FIELD_MAP = {id: :objectId, created_at: :createdAt, updated_at: :updatedAt, acl: :ACL}.freeze
34
-
30
+ # The delete operation hash.
35
31
  DELETE_OP = {"__op"=>"Delete"}.freeze
36
32
 
33
+ # @!visibility private
37
34
  def self.included(base)
38
35
  base.extend(ClassMethods)
39
36
  end
40
37
 
38
+ # The class methods added to Parse::Objects
41
39
  module ClassMethods
42
40
 
43
41
  # The fields method returns a mapping of all local attribute names and their data type.
@@ -64,7 +62,8 @@ module Parse
64
62
  @enums ||= {}
65
63
  end
66
64
 
67
- # Keeps track of all the attributes supported by this class.
65
+ # Set the property fields for this class.
66
+ # @return [Hash]
68
67
  def attributes=(hash)
69
68
  @attributes = BASE.merge(hash)
70
69
  end
@@ -74,6 +73,7 @@ module Parse
74
73
  @attributes ||= BASE.dup
75
74
  end
76
75
 
76
+ # @return [Array] the list of fields that have defaults.
77
77
  def defaults_list
78
78
  @defaults_list ||= []
79
79
  end
@@ -111,7 +111,8 @@ module Parse
111
111
 
112
112
  # allow :bool for :boolean
113
113
  data_type = :boolean if data_type == :bool
114
- data_type = :integer if data_type == :int
114
+ data_type = :geopoint if data_type == :geo_point
115
+ data_type = :integer if data_type == :int || data_type == :number
115
116
 
116
117
  # set defaults
117
118
  opts = { required: false,
@@ -127,11 +128,11 @@ module Parse
127
128
  # it can be overriden by the :field parameter
128
129
  parse_field = opts[:field].to_sym
129
130
  if self.fields[key].present? && BASE_FIELD_MAP[key].nil?
130
- raise DefinitionError, "Property #{self}##{key} already defined with data type #{data_type}"
131
+ raise ArgumentError, "Property #{self}##{key} already defined with data type #{data_type}"
131
132
  end
132
133
  # We keep the list of fields that are on the remote Parse store
133
134
  if self.fields[parse_field].present?
134
- raise DefinitionError, "Alias property #{self}##{parse_field} conflicts with previously defined property."
135
+ raise ArgumentError, "Alias property #{self}##{parse_field} conflicts with previously defined property."
135
136
  end
136
137
  #dirty tracking. It is declared to use with ActiveModel DirtyTracking
137
138
  define_attribute_methods key
@@ -161,12 +162,12 @@ module Parse
161
162
  if is_enum_type
162
163
 
163
164
  unless data_type == :string
164
- raise DefinitionError, "Property #{self}##{parse_field} :enum option is only supported on :string data types."
165
+ raise ArgumentError, "Property #{self}##{parse_field} :enum option is only supported on :string data types."
165
166
  end
166
167
 
167
168
  enum_values = opts[:enum]
168
169
  unless enum_values.is_a?(Array) && enum_values.empty? == false
169
- raise DefinitionError, "Property #{self}##{parse_field} :enum option must be an Array type of symbols."
170
+ raise ArgumentError, "Property #{self}##{parse_field} :enum option must be an Array type of symbols."
170
171
  end
171
172
  opts[:symbolize] = true
172
173
 
@@ -181,11 +182,11 @@ module Parse
181
182
  # If the passed value is true, the methods are prefixed/suffixed with the name of the enum. It is also possible to supply a custom value:
182
183
  prefix = opts[:_prefix]
183
184
  unless opts[:_prefix].nil? || prefix.is_a?(Symbol) || prefix.is_a?(String)
184
- raise DefinitionError, "Enumeration option :_prefix must either be a symbol or string for #{self}##{key}."
185
+ raise ArgumentError, "Enumeration option :_prefix must either be a symbol or string for #{self}##{key}."
185
186
  end
186
187
 
187
188
  unless opts[:_suffix].is_a?(TrueClass) || opts[:_suffix].is_a?(FalseClass)
188
- raise DefinitionError, "Enumeration option :_suffix must either be true or false for #{self}##{key}."
189
+ raise ArgumentError, "Enumeration option :_suffix must either be true or false for #{self}##{key}."
189
190
  end
190
191
 
191
192
  add_suffix = opts[:_suffix] == true
@@ -193,7 +194,7 @@ module Parse
193
194
 
194
195
  class_method_name = prefix_or_key.to_s.pluralize.to_sym
195
196
  if singleton_class.method_defined?(class_method_name)
196
- raise DefinitionError, "You tried to define an enum named `#{key}` for #{self} " + \
197
+ raise ArgumentError, "You tried to define an enum named `#{key}` for #{self} " + \
197
198
  "but this will generate a method `#{self}.#{class_method_name}` " + \
198
199
  " which is already defined. Try using :_suffix or :_prefix options."
199
200
  end
@@ -229,7 +230,7 @@ module Parse
229
230
 
230
231
  #only support symbolization of string data types
231
232
  if symbolize_value && (data_type == :string || data_type == :array) == false
232
- raise DefinitionError, "Tried to symbolize #{self}##{key}, but it is only supported on :string or :array data types."
233
+ raise ArgumentError, "Tried to symbolize #{self}##{key}, but it is only supported on :string or :array data types."
233
234
  end
234
235
 
235
236
  # Here is the where the 'magic' begins. For each property defined, we will
@@ -300,7 +301,7 @@ module Parse
300
301
  # support question mark methods for boolean
301
302
  if data_type == :boolean
302
303
  if self.method_defined?("#{key}?")
303
- puts "Creating boolean helper :#{key}?. Will overwrite existing method #{self}##{key}_increment!."
304
+ puts "Creating boolean helper :#{key}?. Will overwrite existing method #{self}##{key}?."
304
305
  end
305
306
 
306
307
  # returns true if set to true, false otherwise
@@ -406,23 +407,28 @@ module Parse
406
407
 
407
408
  end #ClassMethods
408
409
 
409
- # returns the class level stored field map
410
+ # @return [Hash] a hash mapping of all property fields and their types.
410
411
  def field_map
411
412
  self.class.field_map
412
413
  end
413
414
 
414
- # returns the list of fields
415
+ # @return returns the list of fields
415
416
  def fields(type = nil)
416
417
  self.class.fields(type)
417
418
  end
418
419
 
419
420
  # TODO: We can optimize
421
+ # @return [Hash] returns the list of property attributes for this class.
420
422
  def attributes
421
423
  {__type: :string, :className => :string}.merge!(self.class.attributes)
422
424
  end
423
425
 
424
426
  # support for setting a hash of attributes on the object with a given dirty tracking value
425
427
  # if dirty_track: is set to false (default), attributes are set without dirty tracking.
428
+ # Allos mass assignment of properties with a provided hash.
429
+ # @param hash [Hash] the hash matching the property field names.
430
+ # @param dirty_track [Boolean] whether dirty tracking be enabled
431
+ # @return [Hash]
426
432
  def apply_attributes!(hash, dirty_track: false)
427
433
  return unless hash.is_a?(Hash)
428
434
 
@@ -433,8 +439,8 @@ module Parse
433
439
  end
434
440
  end
435
441
 
436
- # applies a hash of attributes overriding any current value the object has for those
437
- # attributes
442
+ # Supports mass assignment of attributes
443
+ # @return (see #apply_attributes!)
438
444
  def attributes=(hash)
439
445
  return unless hash.is_a?(Hash)
440
446
  # - [:id, :objectId]
@@ -442,12 +448,14 @@ module Parse
442
448
  apply_attributes!(hash, dirty_track: true)
443
449
  end
444
450
 
445
- # returns a hash of attributes (and their new values) that had been changed.
446
- # This will not include any of the base attributes (ex. id, created_at, etc)
447
- # If true is passed as an argument, then all attributes will be included.
448
- # This method is useful for generating an update hash for the Parse PUT API
449
- # TODO: Replace this algorithm with reduce()
451
+ # Returns a hash of attributes for properties that have changed. This will
452
+ # not include any of the base attributes (ex. id, created_at, etc).
453
+ # This method helps generate the change payload that will be sent when saving
454
+ # objects to Parse.
455
+ # @param include_all [Boolean] whether to include all {Parse::Properties::BASE_KEYS} attributes.
456
+ # @return [Hash]
450
457
  def attribute_updates(include_all = false)
458
+ # TODO: Replace this algorithm with reduce()
451
459
  h = {}
452
460
  changed.each do |key|
453
461
  key = key.to_sym
@@ -464,13 +472,19 @@ module Parse
464
472
  h
465
473
  end
466
474
 
467
- # determines if any of the attributes have changed.
475
+ # @return [Boolean] true if any of the attributes have changed.
468
476
  def attribute_changes?
469
477
  changed.any? do |key|
470
478
  fields[key.to_sym].present?
471
479
  end
472
480
  end
473
-
481
+ # Returns a formatted value based on the operation hash and data_type of the property.
482
+ # For some values in Parse, they are specified as operation hashes which could include
483
+ # Add, Remove, Delete, AddUnique and Increment.
484
+ # @param key [Symbol] the name of the property
485
+ # @param val [Hash] the Parse operation hash value.
486
+ # @param data_type [Symbol] The data type of the property.
487
+ # @return [Object]
474
488
  def format_operation(key, val, data_type)
475
489
  return val unless val.is_a?(Hash) && val["__op"].present?
476
490
  op = val["__op"]
@@ -496,6 +510,11 @@ module Parse
496
510
 
497
511
  # this method takes an input value and transforms it to the proper local format
498
512
  # depending on the data type that was set for a particular property key.
513
+ # Return the internal representation of a property value for a given data type.
514
+ # @param key [Symbol] the name of the property
515
+ # @param val [Object] the value to format.
516
+ # @param data_type [Symbol] provide a hint to the data_type of this value.
517
+ # @return [Object]
499
518
  def format_value(key, val, data_type = nil)
500
519
  # if data_type wasn't passed, then get the data_type from the fields hash
501
520
  data_type ||= self.fields[key]
@@ -4,249 +4,252 @@
4
4
  require_relative '../../query'
5
5
 
6
6
  module Parse
7
- # Defines the querying methods applied to a Parse::Object.
8
- module Querying
9
-
10
- # This feature is a small subset of the
11
- # {http://guides.rubyonrails.org/active_record_querying.html#scopes
12
- # ActiveRecord named scopes} feature. Scoping allows you to specify
13
- # commonly-used queries which can be referenced as class method calls and
14
- # are chainable with other scopes. You can use every {Parse::Query}
15
- # method previously covered such as `where`, `includes` and `limit`.
16
- #
17
- # class Article < Parse::Object
18
- # property :published, :boolean
19
- # scope :published, -> { query(published: true) }
20
- # end
21
- #
22
- # This is the same as defining your own class method for the query.
23
- #
24
- # class Article < Parse::Object
25
- # def self.published
26
- # query(published: true)
27
- # end
28
- # end
29
- #
30
- # You can also chain scopes and pass parameters. In addition, boolean and
31
- # enumerated properties have automatically generated scopes for you to use.
32
- #
33
- # class Article < Parse::Object
34
- # scope :published, -> { query(published: true) }
35
- #
36
- # property :comment_count, :integer
37
- # property :category
38
- # property :approved, :boolean
39
- #
40
- # scope :published_and_commented, -> { published.where :comment_count.gt => 0 }
41
- # scope :popular_topics, ->(name) { published_and_commented.where category: name }
42
- # end
43
- #
44
- # # simple scope
45
- # Article.published # => where published is true
46
- #
47
- # # chained scope
48
- # Article.published_and_commented # published is true and comment_count > 0
49
- #
50
- # # scope with parameters
51
- # Article.popular_topic("music") # => popular music articles
52
- # # equivalent: where(published: true, :comment_count.gt => 0, category: name)
53
- #
54
- # # automatically generated scope
55
- # Article.approved(category: "tour") # => where approved: true, category: 'tour'
56
- #
57
- # If you would like to turn off automatic scope generation for property types,
58
- # set the option `:scope` to false when declaring the property.
59
- # @param name [Symbol] the name of the scope.
60
- # @param body [Proc] the proc related to the scope.
61
- # @raise ArgumentError if body parameter does not respond to `call`
62
- # @return [Symbol] the name of the singleton method created.
63
- def scope(name, body)
64
- unless body.respond_to?(:call)
65
- raise ArgumentError, 'The scope body needs to be callable.'
66
- end
7
+ module Core
8
+ # Defines the querying methods applied to a Parse::Object.
9
+ module Querying
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
67
68
 
68
- name = name.to_sym
69
- if respond_to?(name, true)
70
- puts "Creating scope :#{name}. Will overwrite existing method #{self}.#{name}."
71
- end
69
+ name = name.to_sym
70
+ if respond_to?(name, true)
71
+ puts "Creating scope :#{name}. Will overwrite existing method #{self}.#{name}."
72
+ end
72
73
 
73
- define_singleton_method(name) do |*args, &block|
74
+ define_singleton_method(name) do |*args, &block|
74
75
 
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
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
81
82
 
82
- _q = res || query
83
-
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
83
+ _q = res || query
84
+
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
96
100
  klass = nil # help clean up ruby gc
97
- return klass_scope
101
+ return results.send(m, *args, &chained_block)
98
102
  end
99
- klass = nil # help clean up ruby gc
100
- return results.send(m, *args, &chained_block)
101
103
  end
104
+
105
+ Parse::Query.apply_auto_introspection!(_q)
106
+
107
+ return _q if block.nil?
108
+ _q.results(&block)
102
109
  end
103
- return _q if block.nil?
104
- _q.results(&block)
110
+
111
+ end
112
+
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)
105
129
  end
106
130
 
107
- end
108
-
109
-
110
- # Creates a new {Parse::Query} with the given constraints for this class.
111
- # @example
112
- # # assume Post < Parse::Object
113
- # query = Post.query(:updated_at.before => DateTime.now)
114
- # @return [Parse::Query] a new query with the given constraints for this
115
- # Parse::Object subclass.
116
- def query(constraints = {})
117
- Parse::Query.new self.parse_class, constraints
118
- end; alias_method :where, :query
119
-
120
- # @param conditions (see Parse::Query#where)
121
- # @return (see Parse::Query#where)
122
- # @see Parse::Query#where
123
- def literal_where(conditions = {})
124
- query.where(conditions)
125
- end
126
-
127
- # Fetch all matching objects in this collection matching the constraints.
128
- # This will be the most common way when querying Parse objects for a subclass.
129
- # When no block is passed, all objects are returned. Using a block is more memory
130
- # efficient as matching objects are fetched in batches and discarded after the iteration
131
- # is completed.
132
- # @param constraints [Hash] a set of {Parse::Query} constraints.
133
- # @yield a block to iterate with each matching object.
134
- # @example
135
- #
136
- # songs = Song.all( ... expressions ...) # => array of Parse::Objects
137
- # # memory efficient for large amounts of records.
138
- # Song.all( ... expressions ...) do |song|
139
- # # ... do something with song..
140
- # end
141
- #
142
- # @return [Array<Parse::Object>] an array of matching objects. If a block is passed,
143
- # an empty array is returned.
144
- def all(constraints = {limit: :max})
145
- constraints = constraints.reverse_merge({limit: :max})
146
- prepared_query = query(constraints)
147
- return prepared_query.results(&Proc.new) if block_given?
148
- prepared_query.results
149
- end
150
-
151
- # Returns the first item matching the constraint.
152
- # @overload first(count = 1)
153
- # @param count [Interger] The number of items to return.
154
- # @example
155
- # Object.first(2) # => an array of the first 2 objects in the collection.
156
- # @return [Parse::Object] if count == 1
157
- # @return [Array<Parse::Object>] if count > 1
158
- # @overload first(constraints = {})
159
- # @param constraints [Hash] a set of {Parse::Query} constraints.
160
- # @example
161
- # Object.first( :name => "Anthony" )
162
- # @return [Parse::Object] the first matching object.
163
- def first(constraints = {})
164
- fetch_count = 1
165
- if constraints.is_a?(Numeric)
166
- fetch_count = constraints.to_i
167
- constraints = {}
131
+ # Fetch all matching objects in this collection matching the constraints.
132
+ # This will be the most common way when querying Parse objects for a subclass.
133
+ # When no block is passed, all objects are returned. Using a block is more memory
134
+ # efficient as matching objects are fetched in batches and discarded after the iteration
135
+ # is completed.
136
+ # @param constraints [Hash] a set of {Parse::Query} constraints.
137
+ # @yield a block to iterate with each matching object.
138
+ # @example
139
+ #
140
+ # songs = Song.all( ... expressions ...) # => array of Parse::Objects
141
+ # # memory efficient for large amounts of records.
142
+ # Song.all( ... expressions ...) do |song|
143
+ # # ... do something with song..
144
+ # end
145
+ #
146
+ # @return [Array<Parse::Object>] an array of matching objects. If a block is passed,
147
+ # an empty array is returned.
148
+ def all(constraints = {limit: :max})
149
+ constraints = constraints.reverse_merge({limit: :max})
150
+ prepared_query = query(constraints)
151
+ return prepared_query.results(&Proc.new) if block_given?
152
+ prepared_query.results
168
153
  end
169
- constraints.merge!( {limit: fetch_count} )
170
- res = query(constraints).results
171
- return res.first if fetch_count == 1
172
- return res.first fetch_count
173
- end
174
-
175
- # Creates a count request which is more performant when counting objects.
176
- # @example
177
- # # number of songs with a like count greater than 20.
178
- # count = Song.count( :like_count.gt => 20 )
179
- # @param constraints (see #all)
180
- # @return [Interger] the number of records matching the query.
181
- # @see Parse::Query#count
182
- def count(constraints = {})
183
- query(constraints).count
184
- end
185
-
186
- # Find objects matching the constraint ordered by the descending created_at date.
187
- # @param constraints (see #all)
188
- # @return [Array<Parse::Object>]
189
- def newest(constraints = {})
190
- constraints.merge!(order: :created_at.desc)
191
- _q = query(constraints)
192
- _q.define_singleton_method(:method_missing) { |m, *args, &block| self.results.send(m, *args, &block) }
193
- _q
194
- end
195
-
196
- # Find objects matching the constraint ordered by the ascending created_at date.
197
- # @param constraints (see #all)
198
- # @return [Array<Parse::Object>]
199
- def oldest(constraints = {})
200
- constraints.merge!(order: :created_at.asc)
201
- _q = query(constraints)
202
- _q.define_singleton_method(:method_missing) { |m, *args, &block| self.results.send(m, *args, &block) }
203
- _q
204
- end
205
-
206
- # Find objects for a given objectId in this collection.The result is a list
207
- # (or single item) of the objects that were successfully found.
208
- # @example
209
- # Object.find "<objectId>"
210
- # Object.find "<objectId>", "<objectId>"....
211
- # Object.find ["<objectId>", "<objectId>"]
212
- # @param parse_ids [String] the objectId to find.
213
- # @param type [Symbol] the fetching methodology to use if more than one id was passed.
214
- # - *:parallel* : Utilizes parrallel HTTP requests to fetch all objects requested.
215
- # - *:batch* : This uses a batch fetch request using a contained_in clause.
216
- # @param compact [Boolean] whether to remove nil items from the returned array for objects
217
- # that were not found.
218
- # @return [Parse::Object] if only one id was provided as a parameter.
219
- # @return [Array<Parse::Object>] if more than one id was provided as a parameter.
220
- def find(*parse_ids, type: :parallel, compact: true)
221
- # flatten the list of Object ids.
222
- parse_ids.flatten!
223
- parse_ids.compact!
224
- # determines if the result back to the call site is an array or a single result
225
- as_array = parse_ids.count > 1
226
- results = []
227
-
228
- if type == :batch
229
- # use a .in query with the given id as a list
230
- results = self.class.all(:id.in => parse_ids)
231
- else
232
- # use Parallel to make multiple threaded requests for finding these objects.
233
- # The benefit of using this as default is that each request goes to a specific URL
234
- # which is better than Query request (table scan). This in turn allows for caching of
235
- # individual objects.
236
- results = parse_ids.threaded_map do |parse_id|
237
- next nil unless parse_id.present?
238
- response = client.fetch_object(parse_class, parse_id)
239
- next nil if response.error?
240
- Parse::Object.build response.result, parse_class
154
+
155
+ # Returns the first item matching the constraint.
156
+ # @overload first(count = 1)
157
+ # @param count [Interger] The number of items to return.
158
+ # @example
159
+ # Object.first(2) # => an array of the first 2 objects in the collection.
160
+ # @return [Parse::Object] if count == 1
161
+ # @return [Array<Parse::Object>] if count > 1
162
+ # @overload first(constraints = {})
163
+ # @param constraints [Hash] a set of {Parse::Query} constraints.
164
+ # @example
165
+ # Object.first( :name => "Anthony" )
166
+ # @return [Parse::Object] the first matching object.
167
+ def first(constraints = {})
168
+ fetch_count = 1
169
+ if constraints.is_a?(Numeric)
170
+ fetch_count = constraints.to_i
171
+ constraints = {}
241
172
  end
173
+ constraints.merge!( {limit: fetch_count} )
174
+ res = query(constraints).results
175
+ return res.first if fetch_count == 1
176
+ return res.first fetch_count
242
177
  end
243
- # removes any nil items in the array
244
- results.compact! if compact
245
178
 
246
- as_array ? results : results.first
247
- end; alias_method :get, :find
179
+ # Creates a count request which is more performant when counting objects.
180
+ # @example
181
+ # # number of songs with a like count greater than 20.
182
+ # count = Song.count( :like_count.gt => 20 )
183
+ # @param constraints (see #all)
184
+ # @return [Interger] the number of records matching the query.
185
+ # @see Parse::Query#count
186
+ def count(constraints = {})
187
+ query(constraints).count
188
+ end
189
+
190
+ # Find objects matching the constraint ordered by the descending created_at date.
191
+ # @param constraints (see #all)
192
+ # @return [Array<Parse::Object>]
193
+ def newest(constraints = {})
194
+ constraints.merge!(order: :created_at.desc)
195
+ _q = query(constraints)
196
+ _q.define_singleton_method(:method_missing) { |m, *args, &block| self.results.send(m, *args, &block) }
197
+ _q
198
+ end
248
199
 
249
- end # Querying
200
+ # Find objects matching the constraint ordered by the ascending created_at date.
201
+ # @param constraints (see #all)
202
+ # @return [Array<Parse::Object>]
203
+ def oldest(constraints = {})
204
+ constraints.merge!(order: :created_at.asc)
205
+ _q = query(constraints)
206
+ _q.define_singleton_method(:method_missing) { |m, *args, &block| self.results.send(m, *args, &block) }
207
+ _q
208
+ end
209
+
210
+ # Find objects for a given objectId in this collection.The result is a list
211
+ # (or single item) of the objects that were successfully found.
212
+ # @example
213
+ # Object.find "<objectId>"
214
+ # Object.find "<objectId>", "<objectId>"....
215
+ # Object.find ["<objectId>", "<objectId>"]
216
+ # @param parse_ids [String] the objectId to find.
217
+ # @param type [Symbol] the fetching methodology to use if more than one id was passed.
218
+ # - *:parallel* : Utilizes parrallel HTTP requests to fetch all objects requested.
219
+ # - *:batch* : This uses a batch fetch request using a contained_in clause.
220
+ # @param compact [Boolean] whether to remove nil items from the returned array for objects
221
+ # that were not found.
222
+ # @return [Parse::Object] if only one id was provided as a parameter.
223
+ # @return [Array<Parse::Object>] if more than one id was provided as a parameter.
224
+ def find(*parse_ids, type: :parallel, compact: true)
225
+ # flatten the list of Object ids.
226
+ parse_ids.flatten!
227
+ parse_ids.compact!
228
+ # determines if the result back to the call site is an array or a single result
229
+ as_array = parse_ids.count > 1
230
+ results = []
231
+
232
+ if type == :batch
233
+ # use a .in query with the given id as a list
234
+ results = self.class.all(:id.in => parse_ids)
235
+ else
236
+ # use Parallel to make multiple threaded requests for finding these objects.
237
+ # The benefit of using this as default is that each request goes to a specific URL
238
+ # which is better than Query request (table scan). This in turn allows for caching of
239
+ # individual objects.
240
+ results = parse_ids.threaded_map do |parse_id|
241
+ next nil unless parse_id.present?
242
+ response = client.fetch_object(parse_class, parse_id)
243
+ next nil if response.error?
244
+ Parse::Object.build response.result, parse_class
245
+ end
246
+ end
247
+ # removes any nil items in the array
248
+ results.compact! if compact
250
249
 
250
+ as_array ? results : results.first
251
+ end; alias_method :get, :find
251
252
 
253
+ end # Querying
254
+ end
252
255
  end