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,92 @@
1
+ require_relative "properties"
2
+ # This class adds methods to Parse::Objects in order to create a JSON Parse schema
3
+ # in order to support table creation and table alterations.
4
+ module Parse
5
+ module Schema
6
+
7
+ def self.included(base)
8
+ base.extend(ClassMethods)
9
+ end
10
+
11
+ module ClassMethods
12
+
13
+ # returns the schema of the defined class in the Parse JSON format.
14
+ def schema
15
+ sch = { className: parse_class, fields: {} }
16
+ #first go through all the attributes
17
+ attributes.each do |k,v|
18
+ # don't include the base Parse fields
19
+ next if Parse::Properties::BASE.include?(k)
20
+ next if v.nil?
21
+ result = { type: v.to_s.camelize }
22
+ # if it is a basic column property, find the right datatype
23
+ case v
24
+ when :integer, :float
25
+ result[:type] = "Number".freeze
26
+ when :geopoint, :geo_point
27
+ result[:type] = "GeoPoint".freeze
28
+ when :pointer
29
+ result = { type: "Pointer".freeze, targetClass: references[k] }
30
+ when :acl
31
+ result[:type] = "ACL".freeze
32
+ else
33
+ result[:type] = v.to_s.camelize
34
+ end
35
+
36
+ sch[:fields][k] = result
37
+
38
+ end
39
+ #then add all the relational column attributes
40
+ relations.each do |k,v|
41
+ sch[:fields][k] = { type: "Relation".freeze, targetClass: relations[k] }
42
+ end
43
+ sch
44
+ end
45
+
46
+ # updates the remote schema using Parse::Client
47
+ def update_schema(schema_updates = nil)
48
+ schema_updates ||= schema
49
+ client.update_schema parse_class, schema_updates
50
+ end
51
+
52
+ def create_schema
53
+ client.create_schema parse_class, schema
54
+ end
55
+
56
+ # fetches the current schema of this table.
57
+ def fetch_schema
58
+ client.schema parse_class
59
+ end
60
+
61
+ # A class method for non-destructive auto upgrading a remote schema based on the properties
62
+ # and relations you have defined. If the table doesn't exist, we create the schema
63
+ # from scratch - otherwise we fetched the current schema, calculate the differences
64
+ # and add the missing columns. WE DO NOT REMOVE any columns.
65
+ def auto_upgrade!
66
+ response = fetch_schema
67
+ if response.success?
68
+ #let's figure out the diff fields
69
+ remote_fields = response.result["fields"]
70
+ current_schema = schema
71
+ current_schema[:fields] = current_schema[:fields].reduce({}) do |h,(k,v)|
72
+ #if the field does not exist in Parse, then add it to the update list
73
+ h[k] = v if remote_fields[k.to_s].nil?
74
+ h
75
+ end
76
+ return if current_schema[:fields].empty?
77
+ return update_schema( current_schema )
78
+ else
79
+ return create_schema
80
+ end
81
+ #fetch_schema.success? ? update_schema : create_schema
82
+ end
83
+ #def diff(h2);self.dup.delete_if { |k, v| h2[k] == v }.merge(h2.dup.delete_if { |k, v| self.has_key?(k) }); end;
84
+
85
+ end
86
+
87
+ def schema
88
+ self.class.schema
89
+ end
90
+
91
+ end
92
+ end
@@ -0,0 +1,50 @@
1
+ require 'time'
2
+ require 'active_model'
3
+ require 'active_support'
4
+ require 'active_support/inflector'
5
+ require 'active_support/core_ext/object'
6
+ require 'active_model_serializers'
7
+ require_relative 'model'
8
+
9
+ # Parse has a specific date format. One of the supported types is a date string in
10
+ # ISO 8601 format (including milliseconds). The other is a hash object that contains
11
+ # the similar information. When sending data to Parse, we need to use the hash object,
12
+ # but when receiving data from Parse, we may either get the string version or the hash version.
13
+ # To make things easier to use in ruby, th Parse::Date class inherits from the DateTime class.
14
+ # This will allow us to use all the great ActiveSupport methods for date (ex. 3.days.ago) while
15
+ # providing our own encoding for sending to Parse.
16
+ module Parse
17
+ class Date < ::DateTime
18
+ include ::ActiveModel::Model
19
+ include ::ActiveModel::Serializers::JSON
20
+ def self.parse_class; Parse::Model::TYPE_DATE; end;
21
+ def parse_class; self.class.parse_class; end;
22
+ alias_method :__type, :parse_class
23
+
24
+ # called when encoding to JSON.
25
+ def attributes
26
+ { __type: :string, iso: :string }.freeze
27
+ end
28
+
29
+ # this method is defined because it is used by JSON encoding
30
+ def iso
31
+ to_time.utc.iso8601(3) #include milliseconds
32
+ end
33
+
34
+ end
35
+ end
36
+
37
+ # To enable conversion of other date class objects, we will add a mixin to turn
38
+ # Time and DateTime objects to Parse::Date objects
39
+ class Time
40
+ def parse_date
41
+ Parse::Date.parse self.to_s
42
+ end
43
+
44
+ end
45
+
46
+ class DateTime
47
+ def parse_date
48
+ Parse::Date.parse self.to_s
49
+ end
50
+ end
@@ -0,0 +1,127 @@
1
+ require 'active_support'
2
+ require 'active_support/core_ext/object'
3
+ require_relative "model"
4
+ require 'open-uri'
5
+ # Parse File objects are the only non Parse::Object subclass that has a save method.
6
+ # In general, a Parse File needs content and a specific mime-type to be created. When it is saved, it
7
+ # is sent to AWS/S3 to be saved (by Parse), and the result is a new URL pointing to that file.
8
+ # This however is return as a type of File pointer object (hash format)
9
+ # It only has two fields, the absolute URL of the file and the basename of the file.
10
+ # The contents and mime_type are only present when creating a new file locally and are not
11
+ # stored as part of the parse pointer.
12
+ module Parse
13
+
14
+ class File < Model
15
+
16
+ attr_accessor :name, :url
17
+ attr_accessor :contents, :mime_type
18
+ def self.parse_class; TYPE_FILE; end;
19
+ def parse_class; self.class.parse_class; end;
20
+ alias_method :__type, :parse_class
21
+ FIELD_NAME = "name".freeze
22
+ FIELD_URL = "url".freeze
23
+
24
+ # The initializer to create a new file supports different inputs.
25
+ # If the first paramter is a string which starts with 'http', we then download
26
+ # the content of the file (and use the detected mime-type) to set the content and mime_type fields.
27
+ # If the first parameter is a hash, we assume it might be the Parse File hash format which contains url and name fields only.
28
+ # If the first paramter is a Parse::File, then we copy fields over
29
+ # Otherwise, creating a new file requires a name, the actual contents (usually from a File.open("local.jpg").read ) and the mime-type
30
+ def initialize(name, contents = nil, mime_type = "application/octet-stream".freeze)
31
+
32
+ if name.is_a?(String) && name.start_with?("http".freeze) #could be url string
33
+ file = open( name )
34
+ @contents = file.read
35
+ @name = File.basename file.base_uri.to_s
36
+ @mime_type = file.content_type
37
+ elsif name.is_a?(Hash)
38
+ self.attributes = name
39
+ elsif name.is_a?(::File)
40
+ @contents = contents || name.read
41
+ @name = File.basename name.to_path
42
+ elsif name.is_a?(File)
43
+ @name = name.name
44
+ @url = name.url
45
+ else
46
+ @name = name
47
+ @contents = contents
48
+ end
49
+ if @name.blank?
50
+ raise "Invalid Parse::File initialization with name '#{@name}'"
51
+ end
52
+
53
+ @mime_type ||= mime_type
54
+
55
+ end
56
+
57
+ # This creates a new Parse File Object with from a URL, saves it and returns it
58
+ def self.create(url)
59
+ url = url.url if url.is_a?(Parse::File)
60
+ file = self.new(url)
61
+ file.save
62
+ file
63
+ end
64
+
65
+ # A File object is considered saved if the basename of the URL and the name parameters are equal and
66
+ # the name of the file begins with 'tfss'
67
+ def saved?
68
+ @url.present? && @name.present? && @name == File.basename(@url) && @name.start_with?("tfss".freeze)
69
+ end
70
+
71
+ def attributes
72
+ { __type: :string, name: :string, url: :string }.freeze
73
+ end
74
+
75
+ def ==(u)
76
+ return false unless u.is_a?(self.class)
77
+ @url == u.url
78
+ end
79
+
80
+ def attributes=(h)
81
+ if h.is_a?(String)
82
+ @url = h
83
+ @name = File.basename(h)
84
+ elsif h.is_a?(Hash)
85
+ @url = h[FIELD_URL] || h[:url] || @url
86
+ @name = h[FIELD_NAME] || h[:name] || @name
87
+ end
88
+ end
89
+
90
+ # This is a proxy to the ruby ::File.basename method
91
+ def self.basename(file_name, suffix = nil)
92
+ if suffix.nil?
93
+ ::File.basename(file_name)
94
+ else
95
+ ::File.basename(file_name, suffix)
96
+ end
97
+ end
98
+
99
+ # save (create) the file if it has all the proper fields. You cannot update Parse Files.
100
+ def save
101
+ unless saved? || @contents.nil? || @name.nil?
102
+ response = client.create_file(@name, @contents, @mime_type)
103
+ unless response.error?
104
+ result = response.result
105
+ @name = result[FIELD_NAME] || File.basename(result[FIELD_URL])
106
+ @url = result[FIELD_URL]
107
+ end
108
+ end
109
+ saved?
110
+ end
111
+
112
+ end
113
+
114
+ end
115
+
116
+
117
+ class Hash
118
+ # {"name"=>"tfss-cat.jpg", "url"=>"http://files.parsetfss.com/bcf638bb-3db0-4042-b846-7840b345b0d6/tfss-cat.jpg"}
119
+ # This is a helper method that determines whether a hash looks like a Parse::File hash
120
+ def parse_file?
121
+ url = self[Parse::File::FIELD_URL]
122
+ name = self[Parse::File::FIELD_NAME]
123
+ (count == 2 || self["__type".freeze] == Parse::File.parse_class) &&
124
+ url.present? && name.present? &&
125
+ name == ::File.basename(url) && name.start_with?("tfss".freeze)
126
+ end
127
+ end
@@ -0,0 +1,98 @@
1
+
2
+ require_relative "model"
3
+
4
+ module Parse
5
+
6
+ # A basic geo location object in Parse. It represents a location on a map through a
7
+ # latitude and longitue.
8
+ class GeoPoint < Model
9
+
10
+ attr_accessor :latitude, :longitude
11
+ FIELD_LAT = "latitude".freeze
12
+ FIELD_LNG = "longitude".freeze
13
+ alias_method :lat, :latitude
14
+ alias_method :lng, :longitude
15
+ def self.parse_class; TYPE_GEOPOINT; end;
16
+ def parse_class; self.class.parse_class; end;
17
+ alias_method :__type, :parse_class
18
+ # TODO: Validate the ranges of the geo point for valid lat and lng numbers.
19
+ # To create a GeoPoint, you can either pass a hash (ex. {latitude: 32, longitue: -117})
20
+ # or an array (ex. [32,-117]) as the first parameter.
21
+ # You may also pass a GeoPoint object or both a lat/lng pair (Ex. GeoPoint.new(32, -117) )
22
+ def initialize(latitude = nil, longitude = nil)
23
+ @latitude = @longitude = 0.0
24
+ if latitude.is_a?(Hash) || latitude.is_a?(Array)
25
+ self.attributes = latitude
26
+ elsif latitude.is_a?(Numeric) && longitude.is_a?(Numeric)
27
+ @latitude = latitude
28
+ @longitude = longitude
29
+ elsif latitude.is_a?(GeoPoint)
30
+ @latitude = latitude.latitude
31
+ @longitude = latitude.longitude
32
+ end
33
+ end
34
+
35
+ def attributes
36
+ { __type: :string, latitude: :float, longitude: :float }.freeze
37
+ end
38
+
39
+ def max_miles(m)
40
+ [@latitude,@longitude,m]
41
+ end
42
+
43
+ # Setting lat and lng for an GeoPoint can be done using a hash with the attributes set
44
+ # or with an array of two items where the first is the lat and the second is the lng (ex. [32.22,-118.81])
45
+ def attributes=(h)
46
+ if h.is_a?(Hash)
47
+ h.symbolize_keys!
48
+ @latitude = h[:latitude].to_f || h[:lat].to_f || @latitude
49
+ @longitude = h[:longitude].to_f || h[:lng].to_f || @longitude
50
+ elsif h.is_a?(Array) && h.count == 2
51
+ @latitude = h.first.to_f
52
+ @longitude = h.last.to_f
53
+ end
54
+ end
55
+
56
+ def ==(g)
57
+ return false unless g.is_a?(GeoPoint)
58
+ @latitude == g.latitude && @longitude == g.longitude
59
+ end
60
+
61
+ def to_a
62
+ [@latitude,@longitude]
63
+ end
64
+
65
+ def inspect
66
+ "#<GeoPoint [#{@latitude},#{@longitude}]>"
67
+ end
68
+
69
+ # either GeoPoint, array or lat,lng
70
+ def distance_in_miles(geopoint,lng = nil)
71
+ distance_in_km(geopoint, lng) * 0.621371
72
+ end
73
+
74
+ def distance_in_km(geopoint,lng = nil)
75
+ unless geopoint.is_a?(Parse::GeoPoint)
76
+ geopoint = Parse::GeoPoint.new(geopoint, lng)
77
+ end
78
+
79
+ dtor = Math::PI/180
80
+ r = 6378.14
81
+ r_lat1 = self.latitude * dtor
82
+ r_lng1 = self.longitude * dtor
83
+ r_lat2 = geopoint.latitude * dtor
84
+ r_lng2 = geopoint.longitude * dtor
85
+
86
+ delta_lat = r_lat1 - r_lat2
87
+ delta_lng = r_lng1 - r_lng2
88
+
89
+ a = (Math::sin(delta_lat/2.0) ** 2).to_f + (Math::cos(r_lat1) * Math::cos(r_lat2) * ( Math::sin(delta_lng/2.0) ** 2 ) )
90
+ c = 2.0 * Math::atan2(Math::sqrt(a), Math::sqrt(1.0-a))
91
+ d = r * c
92
+ d
93
+ end
94
+
95
+
96
+ end
97
+
98
+ end
@@ -0,0 +1,120 @@
1
+ require 'active_model'
2
+ require 'active_support'
3
+ require 'active_support/inflector'
4
+ require 'active_support/core_ext/object'
5
+ require 'active_model_serializers'
6
+ require_relative '../client'
7
+
8
+ # This is the base model for all Parse object-type classes.
9
+
10
+ module Parse
11
+
12
+ class Model
13
+
14
+ include Client::Connectable # allows easy default Parse::Client access
15
+ include ::ActiveModel::Model
16
+ include ::ActiveModel::Serializers::JSON # support for JSON Serializers
17
+ include ::ActiveModel::Dirty # adds dirty tracking support
18
+ include ::ActiveModel::Conversion
19
+ extend ::ActiveModel::Callbacks # callback support on save, update, delete, etc.
20
+ extend ::ActiveModel::Naming # provides the methods for getting class names from Model classes
21
+
22
+ # General Parse constants
23
+ KEY_CLASS_NAME = 'className'.freeze
24
+ KEY_OBJECT_ID = 'objectId'.freeze
25
+ KEY_CREATED_AT = 'createdAt'.freeze
26
+ KEY_UPDATED_AT = 'updatedAt'.freeze
27
+ CLASS_USER = '_User'.freeze
28
+ CLASS_INSTALLATION = '_Installation'.freeze
29
+ TYPE_FILE = "File".freeze
30
+ TYPE_GEOPOINT = "GeoPoint".freeze
31
+ TYPE_OBJECT = "Object".freeze
32
+ TYPE_DATE = "Date".freeze
33
+ TYPE_BYTES = "Bytes".freeze
34
+ TYPE_POINTER = "Pointer".freeze
35
+ TYPE_RELATION = "Relation".freeze
36
+ TYPE_FIELD = "__type".freeze
37
+
38
+ # To support being able to have different ruby class names from the 'table'
39
+ # names used in Parse, we will need to have a dynamic lookup system where
40
+ # when a parse class name received, we go through all of our subclasses to determine
41
+ # which Parse::Object subclass is responsible for handling this Parse table class.
42
+ # we use @@model_cache to cache the results of the algorithm since we do this frequently
43
+ # when encoding and decoding objects.
44
+ @@model_cache = {}
45
+ def self.autosave_on_create
46
+ @@autosave_on_create ||= false
47
+ end
48
+ def self.autosave_on_create=(bool)
49
+ @@autosave_on_create = bool
50
+ end
51
+
52
+ class << self
53
+
54
+ def raise_on_save_failure
55
+ @global_raise_on_save_failure ||= false
56
+ end
57
+ def raise_on_save_failure=(bool)
58
+ @global_raise_on_save_failure = bool
59
+ end
60
+
61
+ end
62
+
63
+ # class method to find the responsible ruby Parse::Object subclass that handles
64
+ # the provided parse class (str).
65
+ def self.find_class(str)
66
+ return Parse::File if str == TYPE_FILE.freeze
67
+ return Parse::GeoPoint if str == TYPE_GEOPOINT.freeze
68
+ return Parse::Date if str == TYPE_DATE.freeze
69
+ # return Parse::User if str == "User".freeze
70
+ # return Parse::Installation if str == "Installation".freeze
71
+
72
+ str = str.to_s
73
+ # Basically go through all Parse::Object subclasses and see who is has a parse_class
74
+ # set to this string. We will cache the results for future use.
75
+ @@model_cache[str] ||= Parse::Object.descendants.find do |f|
76
+ f.parse_class == str || f.parse_class == "_#{str}"
77
+ end
78
+
79
+ end
80
+
81
+ end
82
+
83
+ end
84
+
85
+
86
+ class String
87
+ # short helper method to provide lower-first-camelcase
88
+ def columnize
89
+ return "objectId" if self == "id"
90
+ camelize(:lower)
91
+ end;
92
+
93
+ #users for properties: ex. :users -> "_User" or :songs -> Song
94
+ def to_parse_class
95
+ final_class = self.singularize.camelize
96
+ klass = Parse::Model.find_class(final_class) || Parse::Model.find_class(self)
97
+ #handles the case that a class has a custom parse table
98
+ final_class = klass.parse_class if klass.present?
99
+ final_class
100
+ end
101
+ end
102
+
103
+ class Symbol
104
+ # for compatibility
105
+ def columnize
106
+ to_s.columnize.to_sym
107
+ end
108
+
109
+ def singularize
110
+ to_s.singularize
111
+ end
112
+
113
+ def camelize
114
+ to_s.camelize
115
+ end
116
+
117
+ def to_parse_class
118
+ to_s.to_parse_class
119
+ end
120
+ end