parse-stack 1.5.3 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
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