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