parse-stack 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +6 -0
  3. data/Gemfile.lock +77 -0
  4. data/LICENSE +20 -0
  5. data/README.md +1281 -0
  6. data/Rakefile +12 -0
  7. data/bin/console +20 -0
  8. data/bin/server +10 -0
  9. data/bin/setup +7 -0
  10. data/lib/parse/api/all.rb +13 -0
  11. data/lib/parse/api/analytics.rb +16 -0
  12. data/lib/parse/api/apps.rb +37 -0
  13. data/lib/parse/api/batch.rb +148 -0
  14. data/lib/parse/api/cloud_functions.rb +18 -0
  15. data/lib/parse/api/config.rb +22 -0
  16. data/lib/parse/api/files.rb +21 -0
  17. data/lib/parse/api/hooks.rb +68 -0
  18. data/lib/parse/api/objects.rb +77 -0
  19. data/lib/parse/api/push.rb +16 -0
  20. data/lib/parse/api/schemas.rb +25 -0
  21. data/lib/parse/api/sessions.rb +11 -0
  22. data/lib/parse/api/users.rb +43 -0
  23. data/lib/parse/client.rb +225 -0
  24. data/lib/parse/client/authentication.rb +59 -0
  25. data/lib/parse/client/body_builder.rb +69 -0
  26. data/lib/parse/client/caching.rb +103 -0
  27. data/lib/parse/client/protocol.rb +15 -0
  28. data/lib/parse/client/request.rb +43 -0
  29. data/lib/parse/client/response.rb +116 -0
  30. data/lib/parse/model/acl.rb +182 -0
  31. data/lib/parse/model/associations/belongs_to.rb +121 -0
  32. data/lib/parse/model/associations/collection_proxy.rb +202 -0
  33. data/lib/parse/model/associations/has_many.rb +218 -0
  34. data/lib/parse/model/associations/pointer_collection_proxy.rb +71 -0
  35. data/lib/parse/model/associations/relation_collection_proxy.rb +134 -0
  36. data/lib/parse/model/bytes.rb +50 -0
  37. data/lib/parse/model/core/actions.rb +499 -0
  38. data/lib/parse/model/core/properties.rb +377 -0
  39. data/lib/parse/model/core/querying.rb +100 -0
  40. data/lib/parse/model/core/schema.rb +92 -0
  41. data/lib/parse/model/date.rb +50 -0
  42. data/lib/parse/model/file.rb +127 -0
  43. data/lib/parse/model/geopoint.rb +98 -0
  44. data/lib/parse/model/model.rb +120 -0
  45. data/lib/parse/model/object.rb +347 -0
  46. data/lib/parse/model/pointer.rb +106 -0
  47. data/lib/parse/model/push.rb +99 -0
  48. data/lib/parse/query.rb +378 -0
  49. data/lib/parse/query/constraint.rb +130 -0
  50. data/lib/parse/query/constraints.rb +176 -0
  51. data/lib/parse/query/operation.rb +66 -0
  52. data/lib/parse/query/ordering.rb +49 -0
  53. data/lib/parse/stack.rb +11 -0
  54. data/lib/parse/stack/version.rb +5 -0
  55. data/lib/parse/webhooks.rb +228 -0
  56. data/lib/parse/webhooks/payload.rb +115 -0
  57. data/lib/parse/webhooks/registration.rb +139 -0
  58. data/parse-stack.gemspec +45 -0
  59. metadata +340 -0
@@ -0,0 +1,121 @@
1
+ require_relative '../pointer'
2
+ require_relative 'collection_proxy'
3
+ require_relative 'pointer_collection_proxy'
4
+ require_relative 'relation_collection_proxy'
5
+
6
+ # BelongsTo relation is the simplies association in which the local
7
+ # table constains a column that points to a foreign table record using
8
+ # a given Parse Pointer. The key of the property is implied to be the
9
+ # name of the class/parse table that contains the foreign associated record.
10
+ # All belongs to relationship column types have the special data type of :pointer.
11
+ module Parse
12
+ module Associations
13
+
14
+ module BelongsTo
15
+
16
+ def self.included(base)
17
+ base.extend(ClassMethods)
18
+ end
19
+
20
+ module ClassMethods
21
+ attr_accessor :references
22
+ # We can keep references to all "belong_to" properties
23
+ def references
24
+ @references ||= {}
25
+ end
26
+
27
+ # belongs_to :featured_song, as: :song, field: :featuredSong, through: :reference
28
+ # belongs_to :artist
29
+ # belongs_to :manager, as: :user
30
+ # These items are added as attributes with the special data type of :pointer
31
+ def belongs_to(key, opts = {})
32
+ opts = {as: key, field: key.to_s.camelize(:lower), required: false}.merge(opts)
33
+ className = opts[:as].to_parse_class
34
+ parse_field = opts[:field].to_sym
35
+ if self.fields[key].present? && Parse::Properties::BASE_FIELD_MAP[key].nil?
36
+ raise Parse::Properties::DefinitionError, "Belongs relation #{self}##{key} already defined with type #{className}"
37
+ end
38
+ if self.fields[parse_field].present?
39
+ raise Parse::Properties::DefinitionError, "Alias belongs_to #{self}##{parse_field} conflicts with previously defined property."
40
+ end
41
+ # store this attribute in the attributes hash with the proper remote column name.
42
+ # we know the type is pointer.
43
+ self.attributes.merge!( parse_field => :pointer )
44
+ # Add them to our list of pointer references
45
+ self.references.merge!( parse_field => className )
46
+ # Add them to the list of fields in our class model
47
+ self.fields.merge!( key => :pointer, parse_field => :pointer )
48
+ # Mapping between local attribute name and the remote column name
49
+ self.field_map.merge!( key => parse_field )
50
+
51
+ # used for dirty tracking
52
+ define_attribute_methods key
53
+
54
+ # validations
55
+ validates_presence_of(key) if opts[:required]
56
+
57
+ # We generate the getter method
58
+ define_method(key) do
59
+ ivar = :"@#{key}"
60
+ val = instance_variable_get ivar
61
+ # We provide autofetch functionality. If the value is nil and the
62
+ # current Parse::Object is a pointer, then let's auto fetch it
63
+ if val.nil? && pointer?
64
+ autofetch!(key)
65
+ val = instance_variable_get ivar
66
+ end
67
+
68
+ # if for some reason we retrieved either from store or fetching a
69
+ # hash, lets try to buid a Pointer of that type.
70
+
71
+ if val.is_a?(Hash) && ( val["__type"].freeze == "Pointer".freeze || val["__type"].freeze == "Object".freeze )
72
+ val = Parse::Object.build val, ( val["className"] || className )
73
+ instance_variable_set ivar, val
74
+ end
75
+ val
76
+ end
77
+
78
+ define_method("#{key}?") do
79
+ self.send(key).is_a?(Parse::Pointer)
80
+ end
81
+
82
+ # A proxy setter method that has dirty tracking enabled.
83
+ define_method("#{key}=") do |val|
84
+ send "#{key}_set_attribute!", val, true
85
+ end
86
+
87
+ # We only support pointers, either by object or by transforming a hash.
88
+ define_method("#{key}_set_attribute!") do |val, track = true|
89
+ if val.is_a?(Hash) && ( val["__type"].freeze == "Pointer".freeze || val["__type"].freeze == "Object".freeze )
90
+ val = Parse::Object.build val, ( val["className"] || className )
91
+ end
92
+ if track == true
93
+ send :"#{key}_will_change!" unless val == instance_variable_get( :"@#{key}" )
94
+ end
95
+ # Never set an object that is not a Parse::Pointer
96
+ if val.nil? || val.is_a?(Parse::Pointer)
97
+ instance_variable_set(:"@#{key}", val)
98
+ else
99
+ warn "[#{self.class}] Invalid value #{val} set for belongs_to field #{key}"
100
+ end
101
+
102
+ end
103
+ # don't create method aliases if the fields are the same
104
+ return if parse_field.to_sym == key.to_sym
105
+
106
+ if self.method_defined?(parse_field) == false
107
+ alias_method parse_field, key
108
+ alias_method "#{parse_field}=", "#{key}="
109
+ alias_method "#{parse_field}_set_attribute!", "#{key}_set_attribute!"
110
+ elsif parse_field.to_sym != :objectId
111
+ warn "Alias belongs_to method #{self}##{parse_field} already defined."
112
+ end
113
+
114
+ end
115
+
116
+ end # ClassMethod
117
+
118
+ end #BelongsTo
119
+ end #Associations
120
+
121
+ end
@@ -0,0 +1,202 @@
1
+ require 'active_model'
2
+ require 'active_support'
3
+ require 'active_support/inflector'
4
+ require 'active_support/core_ext/object'
5
+ require_relative '../pointer'
6
+
7
+ # A collection proxy is a special type of array wrapper that will allow us to
8
+ # notify the parent object about changes to the array. We use a delegate pattern
9
+ # to send notifications to the parent whenever the content of the internal array changes.
10
+ # The main requirement to using the proxy is to provide the list of initial items if any,
11
+ # the owner to be notified and the name of the attribute 'key'. With that, anytime the array
12
+ # will change, we will notify the delegate by sending :'key'_will_change! . The proxy can also
13
+ # be lazy when fetching the contents of the collection. Whenever the collection is accessed and
14
+ # the list is in a "not loaded" state (empty and loaded == false), we will send :'key_fetch!' to the delegate in order to
15
+ # populate the collection.
16
+ module Parse
17
+
18
+ class CollectionProxy
19
+ include ::ActiveModel::Model
20
+ include ::ActiveModel::Dirty
21
+
22
+ attr_accessor :collection, :delegate, :loaded
23
+ attr_reader :delegate, :key
24
+ attr_accessor :parse_class
25
+ # This is to use dirty tracking within the proxy
26
+ define_attribute_methods :collection
27
+ include Enumerable
28
+
29
+ # To initialize a collection, you need to pass the following named parameters
30
+ # collection - the initial items to add to the collection.
31
+ # :delegate - the owner of the object that will receive the notifications.
32
+ # :key - the name of the key to use when sending notifications for _will_change! and _fetch!
33
+ # :parse_class - what Parse class type are the items of the collection.
34
+ # This is used to typecast the objects in the array to a particular Parse Object type.
35
+ def initialize(collection = nil, delegate: nil, key: nil, parse_class: nil)
36
+ @delegate = delegate
37
+ @key = key.to_sym if key.present?
38
+ @collection = collection.is_a?(Array) ? collection : []
39
+ @loaded = @collection.count > 0
40
+ @parse_class = parse_class
41
+ end
42
+
43
+ def loaded?
44
+ @loaded
45
+ end
46
+
47
+ # helper method to forward a message to the delegate
48
+ def forward(method, params = nil)
49
+ return unless @delegate.present? && @delegate.respond_to?(method)
50
+ params.nil? ? @delegate.send(method) : @delegate.send(method, params)
51
+ end
52
+
53
+ def reset!
54
+ @loaded = false
55
+ clear
56
+ end
57
+
58
+ def ==(other_list)
59
+ if other_list.is_a?(Array)
60
+ return @collection == other_list
61
+ elsif other_list.is_a?(Parse::CollectionProxy)
62
+ return @collection == other_list.instance_variable_get(:@collection)
63
+ end
64
+ end
65
+
66
+ def reload!
67
+ reset!
68
+ collection #force reload
69
+ end
70
+
71
+ def clear
72
+ @collection.clear
73
+ end
74
+
75
+ def to_ary
76
+ collection.to_a
77
+ end; alias_method :to_a, :to_ary
78
+
79
+ # lazy loading of a collection. If empty and not loaded, then forward _fetch!
80
+ # to the delegate
81
+ def collection
82
+ if @collection.empty? && @loaded == false
83
+ @collection = forward( :"#{@key}_fetch!" ) || @collection || []
84
+ @loaded = true
85
+ end
86
+
87
+ @collection
88
+ end
89
+
90
+ def collection=(c)
91
+ notify_will_change!
92
+ @collection = c
93
+ end
94
+
95
+ # Method to add items to the collection.
96
+ def add(*items)
97
+ notify_will_change! if items.count > 0
98
+ items.each do |item|
99
+ collection.push item
100
+ end
101
+ @collection
102
+ end; alias_method :push, :add
103
+
104
+ # Remove items from the collection
105
+ def remove(*items)
106
+ notify_will_change! if items.count > 0
107
+ items.each do |item|
108
+ collection.delete item
109
+ end
110
+ @collection
111
+ end; alias_method :delete, :remove
112
+
113
+ def add!(*items)
114
+ return false unless @delegate.respond_to?(:op_add!)
115
+ @delegate.send :op_add!, @key, items
116
+ reset!
117
+ end
118
+
119
+ def add_unique!(*items)
120
+ return false unless @delegate.respond_to?(:op_add_unique!)
121
+ @delegate.send :op_add_unique!, @key, items
122
+ reset!
123
+ end
124
+
125
+ def remove!(*items)
126
+ return false unless @delegate.respond_to?(:op_remove!)
127
+ @delegate.send :op_remove!, @key, items
128
+ reset!
129
+ end
130
+
131
+ def destroy!
132
+ return false unless @delegate.respond_to?(:op_destroy!)
133
+ @delegate.send :op_destroy!, @key
134
+ collection_will_change!
135
+ @collection.clear
136
+ reset!
137
+ end
138
+
139
+ def rollback!
140
+ restore_attributes
141
+ end
142
+
143
+ def clear_changes!
144
+ clear_changes_information
145
+ end
146
+
147
+ def changes_applied!
148
+ changes_applied
149
+ end
150
+
151
+ def first(*args)
152
+ collection.first(*args)
153
+ end
154
+
155
+ def second
156
+ collection.second
157
+ end
158
+
159
+ def last(*args)
160
+ collection.last(*args)
161
+ end
162
+
163
+ def count
164
+ collection.count
165
+ end
166
+
167
+ def as_json(*args)
168
+ collection.as_json(args)
169
+ end
170
+
171
+ def empty?
172
+ collection.empty?
173
+ end
174
+
175
+ # append items to the array
176
+ def <<(*list)
177
+ if list.count > 0
178
+ notify_will_change!
179
+ list.flatten.each { |e| collection.push(e) }
180
+ end
181
+ end
182
+ # we call our own dirty tracking and also forward the call
183
+ def notify_will_change!
184
+ collection_will_change!
185
+ forward "#{@key}_will_change!"
186
+ end
187
+
188
+ # supported iterator
189
+ def each
190
+ return collection.enum_for(:each) unless block_given?
191
+ collection.each &Proc.new
192
+ end
193
+
194
+ def inspect
195
+ "#<#{self.class} changed?=#{changed?} @collection=#{@collection.inspect} >"
196
+ end
197
+
198
+ end
199
+
200
+
201
+
202
+ end
@@ -0,0 +1,218 @@
1
+ require_relative '../pointer'
2
+ require_relative 'collection_proxy'
3
+ require_relative 'pointer_collection_proxy'
4
+ require_relative 'relation_collection_proxy'
5
+
6
+ module Parse
7
+
8
+ module Associations
9
+
10
+ # This module provides has_many functionality to defining Parse::Object classes.
11
+ # There are two main types of a has_many association - array and relation.
12
+ # A has_many array relation, uses a PointerCollectionProxy to store a list of Parse::Object (or pointers)
13
+ # that are stored in the column of the local table. This means we expect a the remote Parse table to contain
14
+ # a column of type array which would contain a set of hash-like Pointers.
15
+ # In the relation case, the object's Parse table has a column, but it points to a separate
16
+ # relational table (join table) which maps both the local class and the foreign class. In this case
17
+ # the type of the column is of "Relation" with a specific class name. This then means that it contains a set of
18
+ # object Ids that we will treat as being part of the foreign table.
19
+ # Ex. If a Artist defines a has_many relation to a Song class through a column called 'favoriteSongs'.
20
+ # Then the Parse type of the favoriteSongs column, contained in the Artist table,
21
+ # would be Relation<Song>. Any objectIds listed in that relation would then
22
+ # be Song object Ids.
23
+ # One thing to note is that when querying relations, the foreign table is the one that needs to be
24
+ # queried in order to retrive the associated object to the local object. For example,
25
+ # if an Artist has a relation to many Song objects, and we wanted to get the list of songs
26
+ # this artist is related to, we would query the Song table passing the specific artist record
27
+ # we are constraining to.
28
+ module HasMany
29
+ def self.included(base)
30
+ base.extend(ClassMethods)
31
+ end
32
+
33
+ module ClassMethods
34
+ attr_accessor :relations
35
+ def relations
36
+ @relations ||= {}
37
+ end
38
+ # Examples:
39
+ # has_many :fans, as: :users, through: :relation, field: "awesomeFans"
40
+ # has_many :songs
41
+ # has_many :likes, as: :users, through: :relation
42
+ # has_many :artists, field: "managedArtists"
43
+ # The first item in the has_many is the name of the local attribute. This will create
44
+ # several methods for accessing the relation type. By default, the remote column name
45
+ # relating to this attribute will be the lower-first-camelcase version of this key.
46
+ # Ex. a relation to :my_songs, would imply that the remote column name is "mySongs". This behavior
47
+ # can be overriden by using the field: option and passing the literal field name in the Parse table.
48
+ # This allows you to use a local attribute name while still having a different remote column name.
49
+ # Since these types of collections are of a particular "type", we will assume that the name of the
50
+ # key is the plural version of the name of the local camelized-named class. Ex. If the property is named :songs, then
51
+ # we will assume there is a local class name defined as 'Song'. This can be overriden by using the as: parameter.
52
+ # This allows you to name your local attribute differently to what the responsible class for this association.
53
+ # Ex. You could define a has_many :favorite_songs property that points to the User class by using the 'as: :songs'. This would
54
+ # imply that the instance object has a set of Song objects through the attribute :favorite_songs.
55
+ # By default, all associations are stored in 'through: :array' form. If you are working with a Parse Relation, you
56
+ # should specify the 'through: :relation' property instead. This will switch the internal storage mechanisms
57
+ # from using a PointerCollectionProxy to a RelationCollectionProxy.
58
+
59
+ def has_many(key, opts = {})
60
+ opts = {through: :array,
61
+ field: key.to_s.camelize(:lower),
62
+ required: false,
63
+ as: key}.merge(opts)
64
+
65
+ klassName = opts[:as].to_parse_class
66
+ parse_field = opts[:field].to_sym
67
+ access_type = opts[:through].to_sym
68
+ # verify that the user did not duplicate properties or defined different properties with the same name
69
+ if self.fields[key].present? && Parse::Properties::BASE_FIELD_MAP[key].nil?
70
+ raise Parse::Properties::DefinitionError, "Has_many property #{self}##{key} already defined with type #{klassName}"
71
+ end
72
+ if self.fields[parse_field].present?
73
+ raise Parse::Properties::DefinitionError, "Alias has_many #{self}##{parse_field} conflicts with previously defined property."
74
+ end
75
+ # validations
76
+ validates_presence_of(key) if opts[:required]
77
+
78
+ # default proxy class.
79
+ proxyKlass = Parse::PointerCollectionProxy
80
+
81
+ #if this is a relation type, use this proxy instead. Relations are stored
82
+ # in the relations hash. If a PointerCollectionProxy is used, we store those
83
+ # as we would normal properties.
84
+ if access_type == :relation
85
+ proxyKlass = Parse::RelationCollectionProxy
86
+ self.relations[key] = klassName
87
+ else
88
+ self.attributes.merge!( parse_field => :array )
89
+ # Add them to the list of fields in our class model
90
+ self.fields.merge!( key => :array, parse_field => :array )
91
+ end
92
+
93
+ self.field_map.merge!( key => parse_field )
94
+ # dirty tracking
95
+ define_attribute_methods key
96
+
97
+ # The first method to be defined is a getter.
98
+ define_method(key) do
99
+ ivar = :"@#{key}"
100
+ val = instance_variable_get(ivar)
101
+ # if the value for this is nil and we are a pointer, then autofetch
102
+ if val.nil? && pointer?
103
+ autofetch!(key)
104
+ val = instance_variable_get ivar
105
+ end
106
+
107
+ # if the result is not a collection proxy, then create a new one.
108
+ unless val.is_a?(Parse::PointerCollectionProxy)
109
+ results = []
110
+ #results = val.parse_objects if val.respond_to?(:parse_objects)
111
+ val = proxyKlass.new results, delegate: self, key: key
112
+ instance_variable_set(ivar, val)
113
+ end
114
+ val
115
+ end
116
+
117
+ # proxy setter that forwards with dirty tracking
118
+ define_method("#{key}=") do |val|
119
+ send "#{key}_set_attribute!", val, true
120
+ end
121
+
122
+ # This will set the content of the proxy.
123
+ define_method("#{key}_set_attribute!") do |val, track = true|
124
+ # If it is a hash, with a __type of Relation, createa a new RelationCollectionProxy, regardless
125
+ # of what is defined because we must have gotten this from Parse.
126
+ val = [] if val.nil?
127
+
128
+ if val.is_a?(Hash) && val["__type"] == "Relation"
129
+ relation_objects = val["objects"] || []
130
+ val = Parse::RelationCollectionProxy.new relation_objects, delegate: self, key: key, parse_class: (val["className"] || klassName)
131
+ elsif val.is_a?(Hash) && val["__op"] == "AddRelation" && val["objects"].present?
132
+ _collection = proxyKlass.new [], delegate: self, key: key, parse_class: (val["className"] || klassName)
133
+ _collection.loaded = true
134
+ _collection.add val["objects"].parse_objects
135
+ val = _collection
136
+ elsif val.is_a?(Hash) && val["__op"] == "RemoveRelation" && val["objects"].present?
137
+ _collection = proxyKlass.new [], delegate: self, key: key, parse_class: (val["className"] || klassName)
138
+ _collection.loaded = true
139
+ _collection.remove val["objects"].parse_objects
140
+ val = _collection
141
+ elsif val.is_a?(Array)
142
+ # Otherwise create a new collection based on what the user defined.
143
+ val = proxyKlass.new val.parse_objects, delegate: self, key: key, parse_class: klassName
144
+ end
145
+
146
+ # send dirty tracking if set
147
+ if track == true
148
+ send :"#{key}_will_change!" unless val == instance_variable_get( :"@#{key}" )
149
+ end
150
+ # TODO: Only allow empty proxy collection class as a value or nil.
151
+ if val.is_a?(Parse::CollectionProxy)
152
+ instance_variable_set(:"@#{key}", val)
153
+ else
154
+ warn "[#{self.class}] Invalid value #{val} for :has_many field #{key}. Should be an Array or a CollectionProxy"
155
+ end
156
+
157
+ end
158
+
159
+ data_type = opts[:through]
160
+ # if the type is a relation association, add these methods to the delegate
161
+ # that will be used when creating the collection proxies. See Collection proxies
162
+ # for more information.
163
+ if data_type == :relation
164
+ # return a query given the foreign table class name.
165
+ define_method("#{key}_relation_query") do
166
+ Parse::Query.new(klassName, key.to_sym.related_to => self.pointer, limit: :max)
167
+ end
168
+ # fetch the contents of the relation
169
+ define_method("#{key}_fetch!") do
170
+ q = self.send :"#{key}_relation_query"
171
+ q.results || []
172
+ end
173
+
174
+ end
175
+
176
+ # if the remote field name and the local field name are the same
177
+ # don't create alias methods
178
+ return if parse_field.to_sym == key.to_sym
179
+
180
+ if self.method_defined?(parse_field) == false
181
+ alias_method parse_field, key
182
+ alias_method "#{parse_field}=", "#{key}="
183
+ alias_method "#{parse_field}_set_attribute!", "#{key}_set_attribute!"
184
+ elsif parse_field.to_sym != :objectId
185
+ warn "Alias has_many method #{self}##{parse_field} already defined."
186
+ end
187
+
188
+
189
+ end # has_many_array
190
+ end #ClassMethods
191
+
192
+ # provides a hash list of all relations to this class.
193
+ def relations
194
+ self.class.relations
195
+ end
196
+
197
+ # returns a has of all the relation changes that have been performed on this
198
+ # instance.
199
+ def relation_updates
200
+ h = {}
201
+ changed.each do |key|
202
+ next unless relations[key.to_sym].present? && send(key).changed?
203
+ remote_field = self.field_map[key.to_sym] || key
204
+ h[remote_field] = send key
205
+ end
206
+ h
207
+ end
208
+
209
+ # true if this object has any relation changes
210
+ def relation_changes?
211
+ changed.any? { |key| relations[key.to_sym] }
212
+ end
213
+
214
+ end # HasMany
215
+ end #Associations
216
+
217
+
218
+ end # Parse