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,103 @@
1
+ require 'faraday'
2
+ require 'faraday_middleware'
3
+ require 'moneta'
4
+ require_relative 'protocol'
5
+ # This is a caching middleware for Parse queries using Moneta.
6
+ module Parse
7
+ module Middleware
8
+ class Caching < Faraday::Middleware
9
+ include Parse::Protocol
10
+ # Internal: List of status codes that can be cached:
11
+ # * 200 - 'OK'
12
+ # * 203 - 'Non-Authoritative Information'
13
+ # * 300 - 'Multiple Choices'
14
+ # * 301 - 'Moved Permanently'
15
+ # * 302 - 'Found'
16
+ # * 404 - 'Not Found'
17
+ # * 410 - 'Gone'
18
+ CACHEABLE_HTTP_CODES = [200, 203, 300, 301, 302, 404, 410].freeze
19
+
20
+ class << self
21
+ attr_accessor :enabled, :logging
22
+
23
+ def enabled
24
+ @enabled = true if @enabled.nil?
25
+ @enabled
26
+ end
27
+
28
+ def caching?
29
+ @enabled
30
+ end
31
+
32
+ end
33
+
34
+ attr_accessor :store, :expires
35
+
36
+ def initialize(app, store, opts = {})
37
+ super(app)
38
+ @store = store
39
+ @opts = {expires: 0}
40
+ @opts.merge!(opts) if opts.is_a?(Hash)
41
+ @expires = @opts[:expires]
42
+
43
+ unless @store.is_a?(Moneta::Transformer)
44
+ raise "Parse::Middleware::Caching store object must a Moneta key/value store."
45
+ end
46
+
47
+ end
48
+
49
+ def call(env)
50
+ dup.call!(env)
51
+ end
52
+
53
+ def call!(env)
54
+
55
+ #unless caching is enabled and we have a valid cache duration
56
+ # then just work as a passthrough
57
+ return @app.call(env) unless @store.present? && @expires > 0 && self.class.enabled
58
+
59
+ cache_enabled = true
60
+
61
+ url = env.url
62
+ method = env.method
63
+ begin
64
+ if method == :get && url.present? && @store.key?(url)
65
+ puts("[Parse::Cache] >>> #{url}") if self.class.logging.present?
66
+ response = Faraday::Response.new
67
+ body = @store[url].body
68
+ if body.present?
69
+ response.finish({status: 200, response_headers: {}, body: body })
70
+ return response
71
+ else
72
+ @store.delete url
73
+ end
74
+ elsif url.present?
75
+ #non GET requets should clear the cache for that same resource path.
76
+ #ex. a POST to /1/classes/Artist/<objectId> should delete the cache for a GET
77
+ # request for the same '/1/classes/Artist/<objectId>' where objectId are equivalent
78
+ @store.delete url
79
+ end
80
+ rescue Exception => e
81
+ # if the cache store fails to connect, catch the exception but proceed
82
+ # with the regular request, but turn off caching for this request. It is possible
83
+ # that the cache connection resumes at a later point, so this is temporary.
84
+ cache_enabled = false
85
+ warn "[Parse::Cache Error] Cache store connection failed. #{e}"
86
+ end
87
+
88
+
89
+ @app.call(env).on_complete do |response_env|
90
+ # Only cache GET requests with valid HTTP status codes.
91
+ if cache_enabled && method == :get && CACHEABLE_HTTP_CODES.include?(response_env.status) && response_env.present?
92
+ @store.store(url, response_env, expires: @expires) # ||= response_env.body
93
+ end
94
+ # do something with the response
95
+ # response_env[:response_headers].merge!(...)
96
+ end
97
+ end
98
+
99
+ end #Caching
100
+
101
+ end #Middleware
102
+
103
+ end
@@ -0,0 +1,15 @@
1
+
2
+ # A module to contain all the main constants.
3
+ module Parse
4
+
5
+ module Protocol
6
+ HOST = "api.parse.com".freeze
7
+ APP_ID = 'X-Parse-Application-Id'.freeze
8
+ API_KEY = 'X-Parse-REST-API-Key'.freeze
9
+ MASTER_KEY = 'X-Parse-Master-Key'.freeze
10
+ SESSION_TOKEN = 'X-Parse-Session-Token'.freeze
11
+ CONTENT_TYPE = "Content-Type".freeze
12
+ CONTENT_TYPE_FORMAT = "application/json; charset=utf-8".freeze
13
+ end
14
+
15
+ end
@@ -0,0 +1,43 @@
1
+
2
+ require 'active_support/json'
3
+
4
+ module Parse
5
+ #This class is mainly to create a potential request - mainly for the batching API.
6
+
7
+ class Request
8
+ attr_accessor :method, :path, :body, :headers
9
+ attr_accessor :tag #for tracking in bulk requests
10
+ def initialize(method, uri, body: nil, headers: nil)
11
+ @tag = 0
12
+ method = method.downcase.to_sym
13
+ raise "Invalid Method type #{method} " unless [:get,:put,:delete,:post].include?(method)
14
+ self.method = method.downcase
15
+ self.path = uri
16
+ self.body = body
17
+ self.headers = headers || {}
18
+ end
19
+
20
+
21
+ def query
22
+ body if @method == :get
23
+ end
24
+
25
+ def as_json
26
+ signature.as_json
27
+ end
28
+
29
+ def ==(r)
30
+ return false unless r.is_a?(Request)
31
+ @method == r.method && @path == r.uri && @body == r.body && @headers == r.headers
32
+ end
33
+
34
+ # signature provies a way for us to compare different requests objects.
35
+ # Two requests objects are the same if they have the same signature.
36
+ # This also helps us serialize a request data into a hash.
37
+ def signature
38
+ {method: @method.upcase, path: @path, body: @body}
39
+ end
40
+
41
+ end
42
+
43
+ end
@@ -0,0 +1,116 @@
1
+ require 'active_support/json'
2
+ # This is the model that represents a response from Parse. A Response can also
3
+ # be a set of responses (from a Batch response).
4
+ module Parse
5
+
6
+ class Response
7
+ include Enumerable
8
+
9
+ ERROR_INTERNAL = 1
10
+ ERROR_TIMEOUT = 124
11
+ ERROR_EXCEEDED_BURST_LIMIT = 155
12
+ ERROR_OBJECT_NOT_FOUND_FOR_GET = 101
13
+
14
+ ERROR = "error".freeze
15
+ CODE = "code".freeze
16
+ RESULTS = "results".freeze
17
+ COUNT = "count".freeze
18
+ # A response has a result or (a code and an error)
19
+ attr_accessor :parse_class, :code, :error, :result
20
+ # You can query Parse for counting objects, which may not actually have
21
+ # results.
22
+ attr_reader :count
23
+
24
+ def initialize(res = {})
25
+ @count = 0
26
+ @batch_response = false # by default, not a batch response
27
+ @result = nil
28
+ # If a string is used for initializing, treat it as JSON
29
+ res = JSON.parse(res) if res.is_a?(String)
30
+ # If it is a hash (or parsed JSON), then parse the result.
31
+ parse_result(res) if res.is_a?(Hash)
32
+ # if the result is an Array, then most likely it is a set of responses
33
+ # from using a Batch API.
34
+ if res.is_a?(Array)
35
+ @batch_response = true
36
+ @result = res || []
37
+ @count = @result.count
38
+ end
39
+ #if none match, set pure result
40
+ @result = res if @result.nil?
41
+
42
+ end
43
+
44
+ def batch?
45
+ @batch_response
46
+ end
47
+ #batch response
48
+ #
49
+ # [
50
+ # {
51
+ # "success":{"createdAt":"2015-11-22T19:04:16.104Z","objectId":"s4tEzOVQFc"}
52
+ # },
53
+ # {
54
+ # "error":{"code":101,"error":"object not found for update"}
55
+ # }
56
+ # ]
57
+ # If it is a batch respnose, we'll create an array of Response objects for each
58
+ # of the ones in the batch.
59
+ def batch_responses
60
+
61
+ return [@result] unless @batch_response
62
+ # if batch response, generate array based on the response hash.
63
+ @result.map do |r|
64
+ next r unless r.is_a?(Hash)
65
+ hash = r["success".freeze] || r["error".freeze]
66
+ Parse::Response.new hash
67
+ end
68
+ end
69
+
70
+ # This method takes the result hash and determines if it is a regular
71
+ # parse query result, object result or a count result. The response should
72
+ # be a hash either containing the result data or the error.
73
+
74
+ def parse_result(h)
75
+ @result = {}
76
+ return unless h.is_a?(Hash)
77
+ @code = h[CODE]
78
+ @error = h[ERROR]
79
+ if h[RESULTS].is_a?(Array)
80
+ @result = h[RESULTS]
81
+ @count = h[COUNT] || @result.count
82
+ else
83
+ @result = h
84
+ @count = 1
85
+ end
86
+
87
+ end
88
+
89
+ # determines if the response is successful.
90
+ def success?
91
+ @code.nil? && @error.nil?
92
+ end
93
+
94
+ def error?
95
+ ! success?
96
+ end
97
+
98
+ # returns the result data from the response. Always returns an array.
99
+ def results
100
+ return [] if @result.nil?
101
+ @result.is_a?(Array) ? @result : [@result]
102
+ end
103
+
104
+ # returns the first thing in the array.
105
+ def first
106
+ @result.is_a?(Array) ? @result.first : @result
107
+ end
108
+
109
+ def each
110
+ return enum_for(:each) unless block_given?
111
+ results.each(&Proc.new)
112
+ self
113
+ end
114
+
115
+ end
116
+ end
@@ -0,0 +1,182 @@
1
+
2
+ # An ACL represents the Parse Permissions object used for each record. In Parse,
3
+ # it is composed a hash-like object that represent Parse::User objectIds and/or Parse::Role
4
+ # names. For each entity (ex. User/Role/Public), you can define read/write priviledges on a particular record.
5
+ # The way they are implemented here is through an internal hash, with each value being of type Parse::ACL::Permission object.
6
+ # A Permission object contains two accessors - read and write - and knows how to generate its JSON
7
+ # structure. In Parse, if you want to give priviledges for an action (ex. read/write), then you set it to true.
8
+ # If you want to deny a priviledge, then you set it to false. One important thing is that when
9
+ # being converted to the Parse format, removing a priviledge means omiting it from the final
10
+ # JSON structure.
11
+ # The class below also implements a type of delegate pattern in order to inform the main Parse::Object
12
+ # of dirty tracking.
13
+ module Parse
14
+
15
+ class ACL
16
+ # The internal permissions hash and delegate accessors
17
+ attr_accessor :permissions, :delegate
18
+ include ::ActiveModel::Model
19
+ include ::ActiveModel::Serializers::JSON
20
+ PUBLIC = "*".freeze # Public priviledges are '*' key in Parse
21
+
22
+ # provide a set of acls and the delegate (for dirty tracking)
23
+ # { '*' => { "read": true, "write": true } }
24
+ def initialize(acls = {}, owner: nil)
25
+ everyone(true, true) # sets Public read/write
26
+ @delegate = owner
27
+ if acls.is_a?(Hash)
28
+ self.attributes = acls
29
+ end
30
+
31
+ end
32
+
33
+ # helper
34
+ def self.permission(read, write = nil)
35
+ ACL::Permission.new(read, write)
36
+ end
37
+
38
+ def permissions
39
+ @permissions ||= {}
40
+ end
41
+
42
+ def ==(other_acl)
43
+ return false unless other_acl.is_a?(self.class)
44
+ return false if permissions.keys != other_acl.permissions.keys
45
+ permissions.keys.all? { |per| permissions[per] == other_acl.permissions[per] }
46
+ end
47
+
48
+ # method to set the Public read/write priviledges ('*'). Alias is 'world'
49
+ def everyone(read, write)
50
+ apply(PUBLIC, read, write)
51
+ permissions[PUBLIC]
52
+ end
53
+ alias_method :world, :everyone
54
+
55
+ # dirty tracking. We will tell the delegate through the acl_will_change!
56
+ # method
57
+ def will_change!
58
+ @delegate.acl_will_change! if @delegate.respond_to?(:acl_will_change!)
59
+ end
60
+
61
+ # removes a permission
62
+ def delete(id)
63
+ id = id.id if id.is_a?(Parse::Pointer)
64
+ if id.present? && permissions.has_key?(id)
65
+ will_change!
66
+ permissions.delete(id)
67
+ end
68
+ end
69
+
70
+ # apply a new permission with a given objectId (or tag)
71
+ def apply(id, read = nil, write = nil)
72
+ id = id.id if id.is_a?(Parse::Pointer)
73
+ return unless id.present?
74
+ # create a new Permissions
75
+ permission = ACL.permission(read, write)
76
+ # if the input is already an Permission object, then set it directly
77
+ permission = read if read.is_a?(Parse::ACL::Permission)
78
+
79
+ if permission.is_a?(ACL::Permission)
80
+ if permissions[id.to_s] != permission
81
+ will_change! # dirty track
82
+ permissions[id.to_s] = permission
83
+ end
84
+ end
85
+
86
+ permissions
87
+ end; alias_method :add, :apply
88
+
89
+ # You can apply a Role as a permission ex. "Admin". This will add the
90
+ # ACL of 'role:Admin' as the key in the permissions hash.
91
+ def apply_role(name, read = nil, write = nil)
92
+ apply("role:#{name}", read, write)
93
+ end; alias_method :add_role, :apply_role
94
+ # Used for object conversion when formatting the input/output value in Parse::Object properties
95
+ def self.typecast(value, delegate = nil)
96
+ ACL.new(value, owner: delegate)
97
+ end
98
+
99
+ # Used for JSON serialization. Only if an attribute is non-nil, do we allow it
100
+ # in the Permissions hash, since omission means denial of priviledge. If the
101
+ # permission value has neither read or write, then the entire record has been denied
102
+ # all priviledges
103
+ def attributes
104
+ permissions.select {|k,v| v.present? }.as_json
105
+ end
106
+
107
+ def attributes=(h)
108
+ return unless h.is_a?(Hash)
109
+ will_change!
110
+ @permissions ||= {}
111
+ h.each do |k,v|
112
+ apply(k,v)
113
+ end
114
+ end
115
+
116
+ def inspect
117
+ "ACL(#{as_json.inspect})"
118
+ end
119
+
120
+ def as_json(*args)
121
+ permissions.select {|k,v| v.present? }.as_json
122
+ end
123
+
124
+ def present?
125
+ permissions.values.any? { |v| v.present? }
126
+ end
127
+
128
+ # Permission class
129
+ class Permission
130
+ include ::ActiveModel::Model
131
+ include ::ActiveModel::Serializers::JSON
132
+ # we don't support changing priviledges directly since it would become
133
+ # crazy to track for dirty tracking
134
+ attr_reader :read, :write
135
+
136
+ # initialize with read and write priviledge
137
+ def initialize(r = nil, w = nil)
138
+ if r.is_a?(Hash)
139
+ r.symbolize_keys!
140
+ # @read = true if r[:read].nil? || r[:read].present?
141
+ # @write = true if r[:write].nil? || r[:write].present?
142
+ @read = r[:read].present?
143
+ @write = r[:write].present?
144
+ else
145
+ # @read = true if r.nil? || r.present?
146
+ # @write = true if w.nil? || w.present?
147
+ @read = r.present?
148
+ @write = w.present?
149
+ end
150
+ end
151
+
152
+ def ==(per)
153
+ return false unless per.is_a?(self.class)
154
+ @read == per.read && @write == per.write
155
+ end
156
+
157
+ # omission or false on a priviledge means don't include it
158
+ def as_json(*args)
159
+ h = {}
160
+ h[:read] = true if @read
161
+ h[:write] = true if @write
162
+ h.empty? ? nil : h.as_json
163
+ end
164
+
165
+ def attributes
166
+ h = {}
167
+ h.merge!(read: :boolean) if @read
168
+ h.merge!(write: :boolean) if @write
169
+ h
170
+ end
171
+
172
+ def inspect
173
+ as_json.inspect
174
+ end
175
+
176
+ def present?
177
+ @read.present? || @write.present?
178
+ end
179
+
180
+ end
181
+ end
182
+ end