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,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