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,130 @@
|
|
1
|
+
require_relative 'operation'
|
2
|
+
require 'time'
|
3
|
+
# Constraints are the heart of the Parse::Query system.
|
4
|
+
# Each constraint is made up of an Operation and a value (the right side
|
5
|
+
# of an operator). Constraints are responsible for making their specific
|
6
|
+
# Parse hash format required when sending Queries to Parse. All constraints can
|
7
|
+
# be combined by merging different constraints (since they are multiple hashes)
|
8
|
+
# and some constraints may have higher precedence than others (ex. equality is higher
|
9
|
+
# precedence than an "in" query).
|
10
|
+
# All constraints should inherit from Parse::Constraint and should
|
11
|
+
# register their specific Operation method (ex. :eq or :lte)
|
12
|
+
# For more information about the query design pattern from DataMapper
|
13
|
+
# that inspired this, see http://datamapper.org/docs/find.html
|
14
|
+
module Parse
|
15
|
+
class Constraint
|
16
|
+
|
17
|
+
attr_accessor :operation, :value
|
18
|
+
# A constraint needs an operation and a value.
|
19
|
+
# You may also pass a block to modify the operation if needed
|
20
|
+
def initialize(operation, value)
|
21
|
+
# if the first parameter is not an Operation, but it is a symbol
|
22
|
+
# it most likely is just the field name, so let's assume they want
|
23
|
+
# the default equality operation.
|
24
|
+
if operation.is_a?(Operation) == false && operation.respond_to?(:to_sym)
|
25
|
+
operation = Operation.new(operation.to_sym, :eq)
|
26
|
+
end
|
27
|
+
@operation = operation
|
28
|
+
@value = value
|
29
|
+
yield(self) if block_given?
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
# Creates a new constraint given an operation and value.
|
34
|
+
def self.create(operation, value)
|
35
|
+
#default to a generic equality constraint if not passed an operation
|
36
|
+
unless operation.is_a?(Parse::Operation) && operation.valid?
|
37
|
+
return self.new(operation, value)
|
38
|
+
end
|
39
|
+
operation.constraint(value)
|
40
|
+
end
|
41
|
+
|
42
|
+
class << self
|
43
|
+
# The class attributes keep track of the Parse key (special Parse
|
44
|
+
# text symbol representing this operation. Ex. local method could be called
|
45
|
+
# .ex, where the Parse Query operation that should be sent out is "$exists")
|
46
|
+
# in this case, key should be set to "$exists"
|
47
|
+
attr_accessor :key
|
48
|
+
# Precedence defines the priority of this operation when merging.
|
49
|
+
# The higher the more priority it will receive.
|
50
|
+
attr_accessor :precedence
|
51
|
+
|
52
|
+
# method to set the keyword for this Constaint (subclasses)
|
53
|
+
def contraint_keyword(k)
|
54
|
+
@key = k
|
55
|
+
end
|
56
|
+
|
57
|
+
def precedence(v = nil)
|
58
|
+
@precedence = 0 if @precedence.nil?
|
59
|
+
@precedence = v unless v.nil?
|
60
|
+
@precedence
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
def precedence
|
66
|
+
self.class.precedence
|
67
|
+
end
|
68
|
+
|
69
|
+
def key
|
70
|
+
self.class.key
|
71
|
+
end
|
72
|
+
|
73
|
+
# All subclasses should register their operation and themselves
|
74
|
+
# as the handler.
|
75
|
+
def self.register(op, klass = self)
|
76
|
+
Operation.register op, klass
|
77
|
+
end
|
78
|
+
|
79
|
+
def operand
|
80
|
+
@operation.operand unless @operation.nil?
|
81
|
+
end
|
82
|
+
def operand=(o)
|
83
|
+
@operation.operand = o unless @operation.nil?
|
84
|
+
end
|
85
|
+
|
86
|
+
def operator
|
87
|
+
@operation.operator unless @operation.nil?
|
88
|
+
end
|
89
|
+
|
90
|
+
def operator=(o)
|
91
|
+
@operation.operator = o unless @operation.nil?
|
92
|
+
end
|
93
|
+
|
94
|
+
def inspect
|
95
|
+
"<#{self.class} #{operator.to_s}(#{operand.inspect}, `#{value}`)>"
|
96
|
+
end
|
97
|
+
|
98
|
+
def as_json(*args)
|
99
|
+
build
|
100
|
+
end
|
101
|
+
|
102
|
+
# subclasses should override the build method depending on how they
|
103
|
+
# need to construct the Parse formatted query hash
|
104
|
+
# The default case below is for supporting equality.
|
105
|
+
# Before the final value is set int he hash, we call formatted_value in case
|
106
|
+
# we need to format the value for particular data types.
|
107
|
+
def build
|
108
|
+
return { @operation.operand => formatted_value } if @operation.operator == :eq || key.nil?
|
109
|
+
{ @operation.operand => { key => formatted_value } }
|
110
|
+
end
|
111
|
+
|
112
|
+
def to_s
|
113
|
+
inspect
|
114
|
+
end
|
115
|
+
|
116
|
+
# This method formats the value based on some specific data types.
|
117
|
+
def formatted_value
|
118
|
+
d = @value
|
119
|
+
d = { __type: "Date", iso: d.iso8601(3) } if d.respond_to?(:iso8601)
|
120
|
+
d = d.pointer if d.respond_to?(:pointer) #simplified query object
|
121
|
+
#d = d.pointer if d.is_a?(Parse::Object) #simplified query object
|
122
|
+
d = d.source if d.is_a?(Regexp)
|
123
|
+
d
|
124
|
+
end
|
125
|
+
|
126
|
+
register :eq, Constraint
|
127
|
+
register :eql, Constraint
|
128
|
+
precedence 100
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,176 @@
|
|
1
|
+
require_relative 'constraint'
|
2
|
+
|
3
|
+
# Eac constraint type is a subclass of Parse::Constraint
|
4
|
+
# We register each keyword (which is the Parse query operator)
|
5
|
+
# and the local operator we want to use. Each of the registered local
|
6
|
+
# operators are added as methods to the Symbol class.
|
7
|
+
# For more information: https://parse.com/docs/rest/guide#queries
|
8
|
+
# For more information about the query design pattern from DataMapper
|
9
|
+
# that inspired this, see http://datamapper.org/docs/find.html
|
10
|
+
module Parse
|
11
|
+
|
12
|
+
class CompoundQueryConstraint < Constraint
|
13
|
+
contraint_keyword :$or
|
14
|
+
register :or
|
15
|
+
|
16
|
+
def build
|
17
|
+
or_clauses = formatted_value
|
18
|
+
or_clauses = [or_clauses] unless or_clauses.is_a?(Array)
|
19
|
+
return { :$or => or_clauses }
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
class LessOrEqualConstraint < Constraint
|
25
|
+
contraint_keyword :$lte
|
26
|
+
register :lte
|
27
|
+
end
|
28
|
+
|
29
|
+
class LessThanConstraint < Constraint
|
30
|
+
contraint_keyword :$lt
|
31
|
+
register :lt
|
32
|
+
end
|
33
|
+
|
34
|
+
class GreaterThanConstraint < Constraint
|
35
|
+
contraint_keyword :$gt
|
36
|
+
register :gt
|
37
|
+
end
|
38
|
+
|
39
|
+
class GreaterOrEqualConstraint < Constraint
|
40
|
+
contraint_keyword :$gte
|
41
|
+
register :gte
|
42
|
+
end
|
43
|
+
|
44
|
+
class NotEqualConstraint < Constraint
|
45
|
+
contraint_keyword :$ne
|
46
|
+
register :not
|
47
|
+
end
|
48
|
+
|
49
|
+
# Mapps all items contained in the array
|
50
|
+
class ContainedInConstraint < Constraint
|
51
|
+
contraint_keyword :$in
|
52
|
+
register :in
|
53
|
+
register :contained_in
|
54
|
+
|
55
|
+
def build
|
56
|
+
val = formatted_value
|
57
|
+
val = [val].compact unless val.is_a?(Array)
|
58
|
+
{ @operation.operand => { key => val } }
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
# Nullabiliity constraint maps $exist Parse clause a bit differently
|
64
|
+
# Parse currently has a bug that if you select items near a location
|
65
|
+
# and want to make sure a different column has a value, you need to
|
66
|
+
# search where the column does not contani a null/undefined value.
|
67
|
+
# Therefore we override the build method to change the operation to a
|
68
|
+
# NotEqualConstraint
|
69
|
+
class NullabilityConstraint < Constraint
|
70
|
+
contraint_keyword :$exists
|
71
|
+
register :null
|
72
|
+
def build
|
73
|
+
# if nullability is equal true, then $exists should be set to false
|
74
|
+
|
75
|
+
if formatted_value == true
|
76
|
+
return { @operation.operand => { key => false} }
|
77
|
+
else
|
78
|
+
#current bug in parse where if you want exists => true with geo queries
|
79
|
+
# we should map it to a "not equal to null" constraint
|
80
|
+
return { @operation.operand => { Parse::NotEqualConstraint.key => nil } }
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
class ExistsConstraint < Constraint
|
87
|
+
contraint_keyword :$exists
|
88
|
+
register :exists
|
89
|
+
def build
|
90
|
+
# if nullability is equal true, then $exists should be set to false
|
91
|
+
return { @operation.operand => { key => formatted_value } }
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
class NotContainedInConstraint < Constraint
|
96
|
+
contraint_keyword :$nin
|
97
|
+
register :not_in
|
98
|
+
register :not_contained_in
|
99
|
+
end
|
100
|
+
|
101
|
+
# All Things must be contained
|
102
|
+
class ContainsAllConstraint < Constraint
|
103
|
+
contraint_keyword :$all
|
104
|
+
register :all
|
105
|
+
register :contains_all
|
106
|
+
end
|
107
|
+
|
108
|
+
class SelectionConstraint < Constraint
|
109
|
+
#This matches a value for a key in the result of a different query
|
110
|
+
contraint_keyword :$select
|
111
|
+
register :select
|
112
|
+
end
|
113
|
+
|
114
|
+
class RejectionConstraint < Constraint
|
115
|
+
#requires that a key's value not match a value for a key in the result of a different query
|
116
|
+
contraint_keyword :$dontSelect
|
117
|
+
register :reject
|
118
|
+
|
119
|
+
end
|
120
|
+
|
121
|
+
class RegularExpressionConstraint < Constraint
|
122
|
+
#Requires that a key's value match a regular expression
|
123
|
+
contraint_keyword :$regex
|
124
|
+
register :like
|
125
|
+
register :regex
|
126
|
+
end
|
127
|
+
|
128
|
+
# Does the propert relational constraint.
|
129
|
+
class RelationQueryConstraint < Constraint
|
130
|
+
# matches objects in a specific column in a different class table
|
131
|
+
contraint_keyword :$relatedTo
|
132
|
+
register :related_to
|
133
|
+
register :rel
|
134
|
+
def build
|
135
|
+
# pointer = formatted_value
|
136
|
+
# unless pointer.is_a?(Parse::Pointer)
|
137
|
+
# raise "Invalid Parse::Pointer passed to :related(#{@operation.operand}) constraint : #{pointer}"
|
138
|
+
# end
|
139
|
+
{ :$relatedTo => { object: formatted_value, key: @operation.operand } }
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
class JoinQueryConstraint < Constraint
|
144
|
+
contraint_keyword :$inQuery
|
145
|
+
register :join
|
146
|
+
register :in_query
|
147
|
+
|
148
|
+
end
|
149
|
+
|
150
|
+
class DisjointQueryConstraint < Constraint
|
151
|
+
contraint_keyword :$notInQuery
|
152
|
+
register :exclude
|
153
|
+
register :not_in_query
|
154
|
+
|
155
|
+
end
|
156
|
+
|
157
|
+
class NearSphereQueryConstraint < Constraint
|
158
|
+
contraint_keyword :$nearSphere
|
159
|
+
register :near
|
160
|
+
|
161
|
+
def build
|
162
|
+
point = formatted_value
|
163
|
+
max_miles = nil
|
164
|
+
if point.is_a?(Array) && point.count > 1
|
165
|
+
max_miles = point[2] if point.count == 3
|
166
|
+
point = { __type: "GeoPoint", latitude: point[0], longitude: point[1] }
|
167
|
+
end
|
168
|
+
if max_miles.present? && max_miles > 0
|
169
|
+
return { @operation.operand => { key => point, :$maxDistanceInMiles => max_miles.to_f } }
|
170
|
+
end
|
171
|
+
{ @operation.operand => { key => point } }
|
172
|
+
end
|
173
|
+
|
174
|
+
end
|
175
|
+
|
176
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'active_support/inflector'
|
2
|
+
|
3
|
+
# The base operation class used in generating queries.
|
4
|
+
# An Operation contains an operand (field) and the
|
5
|
+
# operator (ex. equals, greater than, etc)
|
6
|
+
# Each unique operation type needs a handler that is responsible
|
7
|
+
# for creating a Constraint with a given value.
|
8
|
+
# When creating a new operation, you need to register the operation
|
9
|
+
# method and the class that will be the handler.
|
10
|
+
module Parse
|
11
|
+
class Operation
|
12
|
+
attr_accessor :operand, :operator
|
13
|
+
class << self
|
14
|
+
attr_accessor :operators
|
15
|
+
def operators
|
16
|
+
@operators ||= {}
|
17
|
+
end
|
18
|
+
end
|
19
|
+
# a valid Operation has a handler, operand and operator.
|
20
|
+
def valid?
|
21
|
+
! (@operand.nil? || @operator.nil? || handler.nil?)
|
22
|
+
end
|
23
|
+
|
24
|
+
# returns the constraint class designed to handle this operator
|
25
|
+
def handler
|
26
|
+
Operation.operators[@operator] unless @operator.nil?
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize(field, op)
|
30
|
+
self.operand = field.to_sym
|
31
|
+
self.operand = :objectId if operand == :id
|
32
|
+
self.operator = op.to_sym
|
33
|
+
end
|
34
|
+
|
35
|
+
def inspect
|
36
|
+
"#{operator.inspect}(#{operand.inspect})"
|
37
|
+
end
|
38
|
+
|
39
|
+
# create a new constraint based on the handler that had
|
40
|
+
# been registered with this operation.
|
41
|
+
def constraint(value = nil)
|
42
|
+
handler.new(self, value)
|
43
|
+
end
|
44
|
+
|
45
|
+
# have a way to register an operation type.
|
46
|
+
# Example:
|
47
|
+
# register :eq, MyEqualityHandlerClass
|
48
|
+
# the above registered the equality operator which we define to be
|
49
|
+
# a new method on the Symbol class ('eq'), which when passed a value
|
50
|
+
# we will forward the request to the MyEqualityHandlerClass, so that
|
51
|
+
# for a field called 'name', we can do
|
52
|
+
#
|
53
|
+
# :name.eq (returns operation)
|
54
|
+
# :name.eq(value) # returns constraint provided by the handler
|
55
|
+
#
|
56
|
+
def self.register(op, klass)
|
57
|
+
Operation.operators[op.to_sym] = klass
|
58
|
+
Symbol.send :define_method, op do |value = nil|
|
59
|
+
operation = Operation.new self, op
|
60
|
+
value.nil? ? operation : operation.constraint(value)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# Ordering is implemented similarly as constraints in which we add
|
2
|
+
# special methods to the Symbol class. The developer can then pass one
|
3
|
+
# or an array of fields (as symbols) and call the particular ordering
|
4
|
+
# polarity (ex. :name.asc would create a Parse::Order where we want
|
5
|
+
# things to be sortd by the name field in ascending order)
|
6
|
+
# For more information about the query design pattern from DataMapper
|
7
|
+
# that inspired this, see http://datamapper.org/docs/find.html
|
8
|
+
module Parse
|
9
|
+
class Order
|
10
|
+
# We only support ascending and descending
|
11
|
+
ORDERING = {asc: '', desc: '-'}.freeze
|
12
|
+
attr_accessor :field, :direction
|
13
|
+
|
14
|
+
def initialize(field, order = :asc)
|
15
|
+
@field = field.to_sym || :objectId
|
16
|
+
@direction = order
|
17
|
+
end
|
18
|
+
|
19
|
+
def field=(f)
|
20
|
+
@field = f.to_sym
|
21
|
+
end
|
22
|
+
|
23
|
+
# get the Parse keyword for ordering.
|
24
|
+
def polarity
|
25
|
+
ORDERING[@direction] || ORDERING[:asc]
|
26
|
+
end # polarity
|
27
|
+
|
28
|
+
def to_s
|
29
|
+
"" if @field.nil?
|
30
|
+
polarity + @field.to_s
|
31
|
+
end
|
32
|
+
|
33
|
+
def inspect
|
34
|
+
"#{@direction.to_s}(#{@field.inspect})"
|
35
|
+
end
|
36
|
+
|
37
|
+
end # Order
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
# Add all the operator instance methods to the symbol classes
|
42
|
+
class Symbol
|
43
|
+
Parse::Order::ORDERING.keys.each do |sym|
|
44
|
+
define_method(sym) do
|
45
|
+
Parse::Order.new self, sym
|
46
|
+
end
|
47
|
+
end # each
|
48
|
+
|
49
|
+
end
|
data/lib/parse/stack.rb
ADDED
@@ -0,0 +1,228 @@
|
|
1
|
+
|
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 'rack'
|
8
|
+
require_relative 'client'
|
9
|
+
require_relative 'stack'
|
10
|
+
require_relative 'model/object'
|
11
|
+
require_relative 'webhooks/payload'
|
12
|
+
require_relative 'webhooks/registration'
|
13
|
+
|
14
|
+
|
15
|
+
=begin
|
16
|
+
Some methods take a block, and this pattern frequently appears for a block:
|
17
|
+
|
18
|
+
{|x| x.foo}
|
19
|
+
and people would like to write that in a more concise way. In order to do that,
|
20
|
+
a symbol, the method Symbol#to_proc, implicit class casting, and & operator
|
21
|
+
are used in combination. If you put & in front of a Proc instance in the
|
22
|
+
argument position, that will be interpreted as a block. If you combine
|
23
|
+
something other than a Proc instance with &, then implicit class casting
|
24
|
+
will try to convert that to a Proc instance using to_proc method defined on
|
25
|
+
that object if there is any. In case of a Symbol instance, to_proc works in
|
26
|
+
this way:
|
27
|
+
|
28
|
+
:foo.to_proc # => ->x{x.foo}
|
29
|
+
|
30
|
+
For example, suppose you write like this:
|
31
|
+
|
32
|
+
bar(&:foo)
|
33
|
+
The & operator is combined with :foo, which is not a Proc instance, so implicit class cast applies Symbol#to_proc to it, which gives ->x{x.foo}. The & now applies to this and is interpreted as a block, which gives:
|
34
|
+
|
35
|
+
bar{|x| x.foo}
|
36
|
+
=end
|
37
|
+
|
38
|
+
module Parse
|
39
|
+
|
40
|
+
class Object
|
41
|
+
|
42
|
+
def self.webhook_function(functionName, block = nil)
|
43
|
+
if block_given?
|
44
|
+
Parse::Webhooks.route(:function, functionName, &Proc.new)
|
45
|
+
else
|
46
|
+
block = functionName.to_s.underscore.to_sym if block.blank?
|
47
|
+
block = method(block.to_sym) if block.is_a?(Symbol)
|
48
|
+
Parse::Webhooks.route(:function, functionName, block)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.webhook(type, block = nil)
|
53
|
+
|
54
|
+
if type == :function
|
55
|
+
unless block.is_a?(String) || block.is_a?(Symbol)
|
56
|
+
raise "Invalid Cloud Code function name: #{block}"
|
57
|
+
end
|
58
|
+
Parse::Webhooks.route(:function, block, &Proc.new)
|
59
|
+
# then block must be a symbol or a string
|
60
|
+
else
|
61
|
+
if block_given?
|
62
|
+
Parse::Webhooks.route(type, self, &Proc.new)
|
63
|
+
else
|
64
|
+
Parse::Webhooks.route(type, self, block)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
#if block
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
def update_payload
|
72
|
+
h = attribute_updates
|
73
|
+
if relation_changes?
|
74
|
+
r = relation_change_operations.select { |s| s.present? }.first
|
75
|
+
h.merge!(r) if r.present?
|
76
|
+
end
|
77
|
+
h.merge!(className: parse_class) unless h.empty?
|
78
|
+
h.as_json
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
class Webhooks
|
84
|
+
|
85
|
+
include Client::Connectable
|
86
|
+
extend Webhook::Registration
|
87
|
+
|
88
|
+
HTTP_PARSE_WEBHOOK = "HTTP_X_PARSE_WEBHOOK_KEY".freeze
|
89
|
+
HTTP_PARSE_APPLICATION_ID = "HTTP_X_PARSE_APPLICATION_ID".freeze
|
90
|
+
CONTENT_TYPE = "application/json".freeze
|
91
|
+
|
92
|
+
attr_accessor :key
|
93
|
+
class << self
|
94
|
+
attr_accessor :logging
|
95
|
+
def routes
|
96
|
+
@routes ||= OpenStruct.new( {
|
97
|
+
before_save: {}, after_save: {},
|
98
|
+
before_delete: {}, after_delete: {}, function: {}
|
99
|
+
})
|
100
|
+
end
|
101
|
+
|
102
|
+
def route(type, className, block = nil)
|
103
|
+
type = type.to_s.underscore.to_sym #support camelcase
|
104
|
+
if type != :function && className.respond_to?(:parse_class)
|
105
|
+
className = className.parse_class
|
106
|
+
end
|
107
|
+
className = className.to_s
|
108
|
+
block = Proc.new if block_given?
|
109
|
+
if routes[type].nil? || block.respond_to?(:call) == false
|
110
|
+
raise "Invalid Webhook registration trigger #{type} #{className}"
|
111
|
+
end
|
112
|
+
|
113
|
+
# AfterSave/AfterDelete hooks support more than one
|
114
|
+
if type == :after_save || type == :after_delete
|
115
|
+
|
116
|
+
routes[type][className] ||= []
|
117
|
+
routes[type][className].push block
|
118
|
+
else
|
119
|
+
routes[type][className] = block
|
120
|
+
end
|
121
|
+
#puts "Webhook: #{type} -> #{className}..."
|
122
|
+
end
|
123
|
+
|
124
|
+
def call_route(type, className, payload = nil)
|
125
|
+
type = type.to_s.underscore.to_sym #support camelcase
|
126
|
+
className = className.parse_class if className.respond_to?(:parse_class)
|
127
|
+
className = className.to_s
|
128
|
+
|
129
|
+
return unless routes[type].present? && routes[type][className].present?
|
130
|
+
registry = routes[type][className]
|
131
|
+
|
132
|
+
if registry.is_a?(Array)
|
133
|
+
results = registry.map { |hook| hook.call(payload) }
|
134
|
+
return results.last
|
135
|
+
else
|
136
|
+
return registry.call(payload)
|
137
|
+
end
|
138
|
+
nil
|
139
|
+
end
|
140
|
+
|
141
|
+
def success(data = true)
|
142
|
+
{ success: data }.to_json
|
143
|
+
end
|
144
|
+
|
145
|
+
def error(data = false)
|
146
|
+
{ error: data }.to_json
|
147
|
+
end
|
148
|
+
|
149
|
+
def key
|
150
|
+
@key ||= ENV['PARSE_WEBHOOK_KEY']
|
151
|
+
end
|
152
|
+
|
153
|
+
def call(env)
|
154
|
+
|
155
|
+
request = Rack::Request.new env
|
156
|
+
response = Rack::Response.new
|
157
|
+
|
158
|
+
if @key.present? && @key =! request.env[HTTP_PARSE_WEBHOOK]
|
159
|
+
response.write error("Invalid Parse-Webhook Key")
|
160
|
+
return response.finish
|
161
|
+
end
|
162
|
+
|
163
|
+
unless request.content_type.include?(CONTENT_TYPE)
|
164
|
+
response.write error("Invalid content-type format. Should be application/json.")
|
165
|
+
return response.finish
|
166
|
+
end
|
167
|
+
|
168
|
+
request.body.rewind
|
169
|
+
begin
|
170
|
+
payload = Parse::Payload.new request.body.read
|
171
|
+
rescue Exception => e
|
172
|
+
warn "Invalid webhook payload format: #{e}"
|
173
|
+
response.write error("Invalid payload format. Should be valid JSON.")
|
174
|
+
return response.finish
|
175
|
+
end
|
176
|
+
|
177
|
+
if self.logging.present?
|
178
|
+
if payload.trigger?
|
179
|
+
puts "[ParseWebhooks Request] --> #{payload.trigger_name} #{payload.parse_class}:#{payload.parse_id}"
|
180
|
+
elsif payload.function?
|
181
|
+
puts "[ParseWebhooks Request] --> Function #{payload.function_name}"
|
182
|
+
end
|
183
|
+
if self.logging == :debug
|
184
|
+
puts "[ParseWebhooks Payload] ----------------------------"
|
185
|
+
puts payload.as_json
|
186
|
+
puts "----------------------------------------------------\n"
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
begin
|
191
|
+
result = true
|
192
|
+
if payload.function? && payload.function_name.present?
|
193
|
+
result = Parse::Webhooks.call_route(:function, payload.function_name, payload)
|
194
|
+
elsif payload.trigger? && payload.parse_class.present? && payload.trigger_name.present?
|
195
|
+
# call hooks subscribed to the specific class
|
196
|
+
result = Parse::Webhooks.call_route(payload.trigger_name, payload.parse_class, payload)
|
197
|
+
|
198
|
+
# call hooks subscribed to any class route
|
199
|
+
generic_result = Parse::Webhooks.call_route(payload.trigger_name, "*", payload)
|
200
|
+
result = generic_result if generic_result.present? && result.nil?
|
201
|
+
else
|
202
|
+
puts "[ParseWebhooks] --> Could not find mapping route for #{payload}"
|
203
|
+
end
|
204
|
+
|
205
|
+
result = true if result.blank?
|
206
|
+
if self.logging.present?
|
207
|
+
puts "[ParseWebhooks Response] ----------------------------"
|
208
|
+
puts success(result)
|
209
|
+
puts "----------------------------------------------------\n"
|
210
|
+
end
|
211
|
+
response.write success(result)
|
212
|
+
return response.finish
|
213
|
+
rescue Exception => e
|
214
|
+
puts "[ParseWebhooks Error] >> #{e}"
|
215
|
+
puts e.backtrace
|
216
|
+
response.write error( e.to_s )
|
217
|
+
return response.finish
|
218
|
+
end
|
219
|
+
|
220
|
+
#check if we can handle the type trigger/functionName
|
221
|
+
response.write( success )
|
222
|
+
response.finish
|
223
|
+
end # call
|
224
|
+
|
225
|
+
end #class << self
|
226
|
+
end # Webhooks
|
227
|
+
|
228
|
+
end # Parse
|