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,377 @@
1
+ require 'active_model'
2
+ require 'active_support/inflector'
3
+ require 'active_model_serializers'
4
+ require 'time'
5
+
6
+ =begin
7
+ This module provides support for handling all the different types of column data types
8
+ supported in Parse and mapping them between their remote names with their local ruby named attributes.
9
+ By default, the convention used for naming parameters is that the remote column should be in lower-first-camelcase, (ex. myField, eventAddress), except for
10
+ a few special columns like "id" and "acl".
11
+ Properties are defined when creating subclasses of Parse::Object and using the `property` class method.
12
+
13
+ By defining properties, dynamic methods are created in order to allow getters and setters to be used. We will go into detail below.
14
+
15
+ Each class will have a different copy of attribute mapping and field mappings.
16
+ =end
17
+
18
+ module Parse
19
+
20
+ module Properties
21
+ # This is an exception that is thrown if there is an issue when creating a specific property for a class.
22
+ class DefinitionError < Exception; end;
23
+
24
+ # These are the base types supported by Parse.
25
+ TYPES = [:id, :string, :relation, :integer, :float, :boolean, :date, :array, :file, :geopoint, :bytes, :object, :acl].freeze
26
+ # These are the base mappings of the remote field name types.
27
+ BASE = {objectId: :string, createdAt: :date, updatedAt: :date, ACL: :acl}.freeze
28
+ # The list of properties that are part of all objects
29
+ BASE_KEYS = [:id, :created_at, :updated_at].freeze
30
+ # Default hash map of local attribute name to remote column name
31
+ BASE_FIELD_MAP = {id: :objectId, created_at: :createdAt, updated_at: :updatedAt, acl: :ACL}.freeze
32
+
33
+ def self.included(base)
34
+ base.extend(ClassMethods)
35
+ end
36
+
37
+ module ClassMethods
38
+
39
+ # The fields method returns a mapping of all local attribute names and their data type.
40
+ # if type is passed, we return only the fields that matched that data type
41
+ def fields(type = nil)
42
+ @fields ||= {id: :string, created_at: :date, updated_at: :date, acl: :acl}
43
+ if type.present?
44
+ type = type.to_sym
45
+ return @fields.select { |k,v| v == type }
46
+ end
47
+ @fields
48
+ end
49
+
50
+ # This returns the mapping of local to remote attribute names.
51
+ def field_map
52
+ @field_map ||= BASE_FIELD_MAP.dup
53
+ end
54
+
55
+ # Keeps track of all the attributes supported by this class.
56
+ def attributes=(hash)
57
+ @attributes = BASE.merge(hash)
58
+ end
59
+
60
+ def attributes
61
+ @attributes ||= BASE.dup
62
+ end
63
+
64
+ # property :songs, :array
65
+ # property :my_date, :date, field: "myRemoteCOLUMNName"
66
+ # property :my_int, :integer, required: true, default: ->{ rand(10) }
67
+
68
+ # field: (literal column name in Parse)
69
+ # required: (data_type)
70
+ # default: (value or Proc)
71
+ # alias: Whether to create the remote field alias getter/setters for this attribute
72
+ # This is the class level property method to be used when declaring properties. This helps builds specific methods, formatters
73
+ # and conversion handlers for property storing and saving data for a particular parse class.
74
+ # The first parameter is the name of the local attribute you want to declare with its corresponding data type.
75
+ # Declaring a `property :my_date, :date`, would declare the attribute my_date with a corresponding remote column called
76
+ # "myDate" (lower-first-camelcase) with a Parse data type of Date.
77
+ # You can override the implicit naming behavior by passing the option :field to override.
78
+
79
+ # symbolize: Makes sure the saved and return value locally is in symbol format. useful
80
+ # for enum type fields that are string columns in Parse. Ex. a booking_status for a field
81
+ # could be either "submitted" or "completed" in Parse, however with symbolize, these would
82
+ # be available as :submitted or :completed.
83
+ def property(key, data_type = :string, opts = {})
84
+
85
+ key = key.to_sym
86
+
87
+ if data_type.is_a?(Hash)
88
+ opts.merge!(data_type)
89
+ data_type = :string
90
+ end
91
+ # set defaults
92
+ opts = { required: false,
93
+ alias: true,
94
+ symbolize: false,
95
+ field: key.to_s.camelize(:lower)
96
+ }.merge( opts )
97
+ #By default, the remote field name is a lower-first-camelcase version of the key
98
+ # it can be overriden by the :field parameter
99
+ parse_field = opts[:field].to_sym
100
+ if self.fields[key].present? && BASE_FIELD_MAP[key].nil?
101
+ raise DefinitionError, "Property #{self}##{key} already defined with data type #{data_type}"
102
+ end
103
+ # We keep the list of fields that are on the remote Parse store
104
+ if self.fields[parse_field].present?
105
+ raise DefinitionError, "Alias property #{self}##{parse_field} conflicts with previously defined property."
106
+ end
107
+ #dirty tracking. It is declared to use with ActiveModel DirtyTracking
108
+ define_attribute_methods key
109
+
110
+ # this hash keeps list of attributes (based on remote fields) and their data types
111
+ self.attributes.merge!( parse_field => data_type )
112
+ # this maps all the possible attribute fields and their data types. We use both local
113
+ # keys and remote keys because when we receive a remote object that has the remote field name
114
+ # we need to know what the data type conversion should be.
115
+ self.fields.merge!( key => data_type, parse_field => data_type )
116
+ # This creates a mapping between the local field and the remote field name.
117
+ self.field_map.merge!( key => parse_field )
118
+ #puts "Current Self: #{self} - #{key} = #{self.attributes}"
119
+ # if the field is marked as required, then add validations
120
+ if opts[:required]
121
+ # if integer or float, validate that it's a number
122
+ if data_type == :integer || data_type == :float
123
+ validates_numericality_of key
124
+ end
125
+ # validate that it is not empty
126
+ validates_presence_of key
127
+ end
128
+
129
+ # get the default value if provided (or Proc)
130
+ default_value = opts[:default]
131
+ symbolize_value = opts[:symbolize]
132
+ #only support symbolization of string data types
133
+ if symbolize_value && data_type != :string
134
+ raise 'Symbolization is only supported on :string data types.'
135
+ end
136
+
137
+ # Here is the where the 'magic' begins. For each property defined, we will
138
+ # generate special setters and getters that will take advantage of ActiveModel
139
+ # helpers.
140
+
141
+ # We define a getter with the key
142
+ define_method(key) do
143
+
144
+ # we will get the value using the internal value of the instance variable
145
+ # using the instance_variable_get
146
+ ivar = :"@#{key}"
147
+ value = instance_variable_get ivar
148
+
149
+ # If the value is nil and this current Parse::Object instance is a pointer?
150
+ # then someone is calling the getter for this, which means they probably want
151
+ # its value - so let's go turn this pointer into a full object record
152
+ if value.nil? && pointer?
153
+ # call autofetch to fetch the entire record
154
+ # and then get the ivar again cause it might have been updated.
155
+ autofetch!(key)
156
+ value = instance_variable_get ivar
157
+ end
158
+
159
+ # if value is nil (even after fetching), then lets see if the developer
160
+ # set a default value for this attribute.
161
+ if value.nil? && default_value.present?
162
+ # If the default object provided is a Proc, then run the proc, otherwise
163
+ # we'll assume it's just a plain literal value
164
+ value = default_value.is_a?(Proc) ? default_value.call : default_value
165
+ # lets set the variable with the updated value
166
+ instance_variable_set ivar, value
167
+ end
168
+
169
+ # if the value is a String (like an iso8601 date) and the data type of
170
+ # this object is :date, then let's be nice and create a parse date for it.
171
+ if value.is_a?(String) && data_type == :date
172
+ value = Parse::Date.parse value
173
+ instance_variable_set ivar, value
174
+ end
175
+ # finally return the value
176
+ symbolize_value && value.respond_to?(:to_sym) ? value.to_sym : value
177
+ end
178
+
179
+ # The second method to be defined is a setter method. This is done by
180
+ # defining :key with a '=' sign. However, to support setting the attribute
181
+ # with and without dirty tracking, we really will just proxy it to another method
182
+
183
+ define_method("#{key}=") do |val|
184
+ #we proxy the method passing the value and true. Passing true to the
185
+ # method tells it to make sure dirty tracking is enabled.
186
+ self.send "#{key}_set_attribute!", val, true
187
+ end
188
+
189
+ # This is the real setter method. Takes two arguments, the value to set
190
+ # and whether to mark it as dirty tracked.
191
+ define_method("#{key}_set_attribute!") do |val, track = true|
192
+ # Each value has a data type, based on that we can treat the incoming
193
+ # value as input, and format it to the correct storage format. This method is
194
+ # defined in this file (instance method)
195
+ val = format_value(key, val, data_type)
196
+ # if dirty trackin is enabled, call the ActiveModel required method of _will_change!
197
+ # this will grab the current value and keep a copy of it - but we only do this if
198
+ # the new value being set is different from the current value stored.
199
+ if track == true
200
+ send :"#{key}_will_change!" unless val == instance_variable_get( :"@#{key}" )
201
+ end
202
+ if symbolize_value && data_type == :string
203
+ val = nil if val.blank?
204
+ val = val.to_sym if val.respond_to?(:to_sym)
205
+ end
206
+ # now set the instance value
207
+ instance_variable_set :"@#{key}", val
208
+ end
209
+
210
+ # The core methods above support all attributes with the base local :key parameter
211
+ # however, for ease of use and to handle that the incoming fields from parse have different
212
+ # names, we will alias all those methods defined above with the defined parse_field.
213
+ # if both the local name matches the calculated/provided remote column name, don't create
214
+ # an alias method since it is the same thing. Ex. attribute 'username' would probably have the
215
+ # remote column name also called 'username'.
216
+ return if parse_field == key
217
+
218
+ # we will now create the aliases, however if the method is already defined
219
+ # we warn the user unless the field is :objectId since we are in charge of that one.
220
+ # this is because it is possible they want to override. You can turn off this
221
+ # behavior by passing false to :alias
222
+
223
+ if self.method_defined?(parse_field) == false && opts[:alias]
224
+ alias_method parse_field, key
225
+ alias_method "#{parse_field}=", "#{key}="
226
+ alias_method "#{parse_field}_set_attribute!", "#{key}_set_attribute!"
227
+ elsif parse_field.to_sym != :objectId
228
+ warn "Alias property method #{self}##{parse_field} already defined."
229
+ end
230
+
231
+ end # property
232
+
233
+ end #ClassMethods
234
+
235
+ # returns the class level stored field map
236
+ def field_map
237
+ self.class.field_map
238
+ end
239
+
240
+ # returns the list of fields
241
+ def fields(type = nil)
242
+ self.class.fields(type)
243
+ end
244
+
245
+ def attributes
246
+ {__type: :string, :className => :string}.merge!(self.class.attributes)
247
+ end
248
+
249
+ # support for setting a hash of attributes on the object with a given dirty tracking value
250
+ # if dirty_track: is set to false (default), attributes are set without dirty tracking.
251
+ def apply_attributes!(hash, dirty_track: false)
252
+ return unless hash.is_a?(Hash)
253
+
254
+ @id ||= hash["id"] || hash["objectId"]
255
+ hash.each do |key, value|
256
+ method = "#{key}_set_attribute!"
257
+ send(method, value, dirty_track) if respond_to?( method )
258
+ end
259
+ end
260
+
261
+ # applies a hash of attributes overriding any current value the object has for those
262
+ # attributes
263
+ def attributes=(hash)
264
+ return unless hash.is_a?(Hash)
265
+ # - [:id, :objectId]
266
+ # only overwrite @id if it hasn't been set.
267
+ apply_attributes!(hash, dirty_track: true)
268
+ end
269
+
270
+ # returns a hash of attributes (and their new values) that had been changed.
271
+ # This will not include any of the base attributes (ex. id, created_at, etc)
272
+ # If true is passed as an argument, then all attributes will be included.
273
+ # This method is useful for generating an update hash for the Parse PUT API
274
+ # TODO: Replace this algorithm with reduce()
275
+ def attribute_updates(include_all = false)
276
+ h = {}
277
+ changed.each do |key|
278
+ key = key.to_sym
279
+ next if include_all == false && Parse::Properties::BASE_KEYS.include?(key)
280
+ next unless fields[key].present?
281
+ remote_field = self.field_map[key] || key
282
+ h[remote_field] = send key
283
+ h[remote_field] = {__op: :Delete} if h[remote_field].nil?
284
+ # in the case that the field is a Parse object, generate a pointer
285
+ h[remote_field] = h[remote_field].pointer if h[remote_field].respond_to?(:pointer)
286
+ end
287
+ h
288
+ end
289
+
290
+ # determines if any of the attributes have changed.
291
+ def attribute_changes?
292
+ changed.any? do |key|
293
+ fields[key.to_sym].present?
294
+ end
295
+ end
296
+
297
+ def format_operation(key, val, data_type)
298
+ return val unless val.is_a?(Hash) && val["__op"].present?
299
+ op = val["__op"]
300
+ #handles delete case otherwise 'null' shows up in column
301
+ if "Delete" == op
302
+ val = nil
303
+ elsif "Add" == op && data_type == :array
304
+ val = (instance_variable_get(:"@#{key}") || []).to_a + (val["objects"] || [])
305
+ elsif "Remove" == op && data_type == :array
306
+ val = (instance_variable_get(:"@#{key}") || []).to_a - (val["objects"] || [])
307
+ elsif "AddUnique" == op && data_type == :array
308
+ objects = (val["objects"] || []).uniq
309
+ original_items = (instance_variable_get(:"@#{key}") || []).to_a
310
+ objects.reject! { |r| original_items.include?(r) }
311
+ val = original_items + objects
312
+ elsif "Increment" == op && data_type == :integer || data_type == :integer
313
+ # for operations that increment by a certain amount, they come as a hash
314
+ val = (instance_variable_get(:"@#{key}") || 0) + (val["amount"] || 0).to_i
315
+ end
316
+ val
317
+ end
318
+
319
+ # this method takes an input value and transforms it to the proper local format
320
+ # depending on the data type that was set for a particular property key.
321
+ def format_value(key, val, data_type = nil)
322
+ # if data_type wasn't passed, then get the data_type from the fields hash
323
+ data_type ||= self.fields[key]
324
+
325
+ val = format_operation(key, val, data_type)
326
+
327
+ case data_type
328
+ when :object
329
+ val = val #should be regular hash, maybe in the future we return hashie?
330
+ when :array
331
+ # All "array" types use a collection proxy
332
+ val = [val] unless val.is_a?(Array) #all objects must be in array form
333
+ val.compact! #remove any nil
334
+ val = Parse::CollectionProxy.new val, delegate: self, key: key
335
+ when :geopoint
336
+ val = Parse::GeoPoint.new(val) unless val.blank?
337
+ when :file
338
+ val = Parse::File.new(val) unless val.blank?
339
+ when :bytes
340
+ val = Parse::Bytes.new(val) unless val.blank?
341
+ when :integer
342
+ val = val.to_i unless val.blank?
343
+ when :boolean
344
+ val = val ? true : false
345
+ when :string
346
+ val = val.to_s unless val.blank?
347
+ when :float
348
+ val = val.to_f unless val.blank?
349
+ when :acl
350
+ # ACL types go through a special conversion
351
+ val = ACL.typecast(val, self)
352
+ when :date
353
+ # if it respond to parse_date, then use that as the conversion.
354
+ if val.respond_to?(:parse_date)
355
+ val = val.parse_date
356
+ # if the value is a hash, then it may be the Parse hash format for an iso date.
357
+ elsif val.is_a?(Hash) # val.respond_to?(:iso8601)
358
+ val = Parse::Date.parse(val["iso".freeze] || val[:iso])
359
+ elsif val.is_a?(String)
360
+ # if it's a string, try parsing the date
361
+ val = Parse::Date.parse val
362
+ end
363
+ else
364
+ # You can provide a specific class instead of a symbol format
365
+ if data_type.respond_to?(:typecast)
366
+ val = data_type.typecast(val)
367
+ else
368
+ warn "Property :#{key}: :#{data_type} has not valid data type"
369
+ val = val #default
370
+ end
371
+ end
372
+ val
373
+ end
374
+
375
+ end # Properties
376
+
377
+ end # Parse
@@ -0,0 +1,100 @@
1
+ require_relative '../../query'
2
+
3
+ # This module provides most of the querying methods for Parse Objects.
4
+ # It proxies much of the query methods to the Parse::Query object.
5
+ module Parse
6
+
7
+ module Querying
8
+
9
+ def self.included(base)
10
+ base.extend(ClassMethods)
11
+ end
12
+
13
+ module ClassMethods
14
+
15
+ # This query method helper returns a Query object tied to a parse class.
16
+ # The parse class should be the name of the one that will be sent in the query
17
+ # request pointing to the remote table.
18
+ def query(constraints = {})
19
+ Parse::Query.new self.parse_class, constraints
20
+ end
21
+
22
+ def where(clauses = {})
23
+ query.where(clauses)
24
+ end
25
+
26
+ # Most common method to use when querying a class. This takes a hash of constraints
27
+ # and conditions and returns the results.
28
+ def all(constraints = {})
29
+ constraints = {limit: :max}.merge(constraints)
30
+ prepared_query = query(constraints)
31
+ return prepared_query.results(&Proc.new) if block_given?
32
+ prepared_query.results
33
+ end
34
+
35
+ # returns the first item matching the constraint. If constraint parameter is numeric,
36
+ # then we treat it as a count.
37
+ # Ex. Object.first( :name => "Anthony" ) (returns single object)
38
+ # Ex. Object.first(3) # first 3 objects (array of 3 objects)
39
+ def first(constraints = {})
40
+ fetch_count = 1
41
+ if constraints.is_a?(Numeric)
42
+ fetch_count = constraints.to_i
43
+ constraints = {}
44
+ end
45
+ constraints.merge!( {limit: fetch_count} )
46
+ res = query(constraints).results
47
+ return res.first if fetch_count == 1
48
+ return res.first fetch_count
49
+ end
50
+
51
+ # creates a count request (which is more performant when counting objects)
52
+ def count(constraints = {})
53
+ query(constraints).count
54
+ end
55
+
56
+ # Find objects based on objectIds. The result is a list (or single item) of the
57
+ # objects that were successfully found.
58
+ # Example:
59
+ # Object.find "<objectId>"
60
+ # Object.find "<objectId>", "<objectId>"....
61
+ # Object.find ["<objectId>", "<objectId>"]
62
+ # Additional named parameters:
63
+ # type: - :parrallel by default - makes all find requests in parallel vs serial.
64
+ # :batch - makes a single query request for all objects with a "contained in" query.
65
+ # compact: - true by default, removes any nil values from the array as it is potential
66
+ # that an object with a specified ID does not exist.
67
+
68
+ def find(*parse_ids, type: :parallel, compact: true)
69
+ # flatten the list of Object ids.
70
+ parse_ids.flatten!
71
+ # determines if the result back to the call site is an array or a single result
72
+ as_array = parse_ids.count > 1
73
+ results = []
74
+
75
+ if type == :batch
76
+ # use a .in query with the given id as a list
77
+ results = self.class.all(:id.in => parse_ids)
78
+ else
79
+ # use Parallel to make multiple threaded requests for finding these objects.
80
+ # The benefit of using this as default is that each request goes to a specific URL
81
+ # which is better than Query request (table scan). This in turn allows for caching of
82
+ # individual objects.
83
+ results = parse_ids.threaded_map do |parse_id|
84
+ response = client.fetch_object(parse_class, parse_id)
85
+ next nil if response.error?
86
+ Parse::Object.build response.result, parse_class
87
+ end
88
+ end
89
+ # removes any nil items in the array
90
+ results.compact! if compact
91
+
92
+ as_array ? results : results.first
93
+ end; alias_method :get, :find
94
+
95
+ end # ClassMethods
96
+
97
+ end # Querying
98
+
99
+
100
+ end