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