parse-stack 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +77 -0
- data/LICENSE +20 -0
- data/README.md +1281 -0
- data/Rakefile +12 -0
- data/bin/console +20 -0
- data/bin/server +10 -0
- data/bin/setup +7 -0
- data/lib/parse/api/all.rb +13 -0
- data/lib/parse/api/analytics.rb +16 -0
- data/lib/parse/api/apps.rb +37 -0
- data/lib/parse/api/batch.rb +148 -0
- data/lib/parse/api/cloud_functions.rb +18 -0
- data/lib/parse/api/config.rb +22 -0
- data/lib/parse/api/files.rb +21 -0
- data/lib/parse/api/hooks.rb +68 -0
- data/lib/parse/api/objects.rb +77 -0
- data/lib/parse/api/push.rb +16 -0
- data/lib/parse/api/schemas.rb +25 -0
- data/lib/parse/api/sessions.rb +11 -0
- data/lib/parse/api/users.rb +43 -0
- data/lib/parse/client.rb +225 -0
- data/lib/parse/client/authentication.rb +59 -0
- data/lib/parse/client/body_builder.rb +69 -0
- data/lib/parse/client/caching.rb +103 -0
- data/lib/parse/client/protocol.rb +15 -0
- data/lib/parse/client/request.rb +43 -0
- data/lib/parse/client/response.rb +116 -0
- data/lib/parse/model/acl.rb +182 -0
- data/lib/parse/model/associations/belongs_to.rb +121 -0
- data/lib/parse/model/associations/collection_proxy.rb +202 -0
- data/lib/parse/model/associations/has_many.rb +218 -0
- data/lib/parse/model/associations/pointer_collection_proxy.rb +71 -0
- data/lib/parse/model/associations/relation_collection_proxy.rb +134 -0
- data/lib/parse/model/bytes.rb +50 -0
- data/lib/parse/model/core/actions.rb +499 -0
- data/lib/parse/model/core/properties.rb +377 -0
- data/lib/parse/model/core/querying.rb +100 -0
- data/lib/parse/model/core/schema.rb +92 -0
- data/lib/parse/model/date.rb +50 -0
- data/lib/parse/model/file.rb +127 -0
- data/lib/parse/model/geopoint.rb +98 -0
- data/lib/parse/model/model.rb +120 -0
- data/lib/parse/model/object.rb +347 -0
- data/lib/parse/model/pointer.rb +106 -0
- data/lib/parse/model/push.rb +99 -0
- data/lib/parse/query.rb +378 -0
- data/lib/parse/query/constraint.rb +130 -0
- data/lib/parse/query/constraints.rb +176 -0
- data/lib/parse/query/operation.rb +66 -0
- data/lib/parse/query/ordering.rb +49 -0
- data/lib/parse/stack.rb +11 -0
- data/lib/parse/stack/version.rb +5 -0
- data/lib/parse/webhooks.rb +228 -0
- data/lib/parse/webhooks/payload.rb +115 -0
- data/lib/parse/webhooks/registration.rb +139 -0
- data/parse-stack.gemspec +45 -0
- 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
|