parse-stack 1.0.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 (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