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,99 @@
|
|
1
|
+
require_relative '../query.rb'
|
2
|
+
require_relative '../client.rb'
|
3
|
+
|
4
|
+
module Parse
|
5
|
+
|
6
|
+
class Push
|
7
|
+
include Client::Connectable
|
8
|
+
attr_accessor :query, :alert, :badge, :sound, :title, :data
|
9
|
+
attr_accessor :expiration_time, :expiration_interval, :push_time, :channels
|
10
|
+
|
11
|
+
alias_method :message, :alert
|
12
|
+
alias_method :message=, :alert=
|
13
|
+
|
14
|
+
def self.send(payload)
|
15
|
+
client.push payload.as_json
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(constraints = {})
|
19
|
+
self.where constraints
|
20
|
+
end
|
21
|
+
|
22
|
+
def query
|
23
|
+
@query ||= Parse::Query.new(Parse::Model::CLASS_INSTALLATION)
|
24
|
+
end
|
25
|
+
|
26
|
+
def where=(where_clausees)
|
27
|
+
query.where where_clauses
|
28
|
+
end
|
29
|
+
|
30
|
+
def where(constraints = nil)
|
31
|
+
return query.compile_where unless constraints.is_a?(Hash)
|
32
|
+
query.where constraints
|
33
|
+
query
|
34
|
+
end
|
35
|
+
|
36
|
+
def channels=(list)
|
37
|
+
@channels = [list].flatten
|
38
|
+
end
|
39
|
+
|
40
|
+
def data=(h)
|
41
|
+
if h.is_a?(String)
|
42
|
+
@alert = h
|
43
|
+
else
|
44
|
+
@data = h.symbolize_keys
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def as_json(*args)
|
49
|
+
payload.as_json
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_json(*args)
|
53
|
+
as_json.to_json
|
54
|
+
end
|
55
|
+
|
56
|
+
def payload
|
57
|
+
msg = {
|
58
|
+
data: {
|
59
|
+
alert: alert,
|
60
|
+
badge: badge || "Increment".freeze
|
61
|
+
}
|
62
|
+
}
|
63
|
+
msg[:data][:sound] = sound if sound.present?
|
64
|
+
msg[:data][:title] = title if title.present?
|
65
|
+
msg[:data].merge! @data if @data.is_a?(Hash)
|
66
|
+
|
67
|
+
|
68
|
+
if @expiration_time.present?
|
69
|
+
msg[:expiration_time] = @expiration_time.respond_to?(:iso8601) ? @expiration_time.iso8601(3) : @expiration_time
|
70
|
+
end
|
71
|
+
if @push_time.present?
|
72
|
+
msg[:push_time] = @push_time.respond_to?(:iso8601) ? @push_time.iso8601(3) : @push_time
|
73
|
+
end
|
74
|
+
|
75
|
+
if @expiration_interval.is_a?(Numeric)
|
76
|
+
msg[:expiration_interval] = @expiration_interval.to_i
|
77
|
+
end
|
78
|
+
|
79
|
+
if query.where.present?
|
80
|
+
q = @query.dup
|
81
|
+
if @channels.is_a?(Array) && @channels.empty? == false
|
82
|
+
q.where :channels.in => @channels
|
83
|
+
end
|
84
|
+
msg[:where] = q.compile_where unless q.where.empty?
|
85
|
+
elsif @channels.is_a?(Array) && @channels.empty? == false
|
86
|
+
msg[:channels] = @channels
|
87
|
+
end
|
88
|
+
msg
|
89
|
+
end
|
90
|
+
|
91
|
+
def send(message = nil)
|
92
|
+
@alert = message if message.is_a?(String)
|
93
|
+
@data = message if message.is_a?(Hash)
|
94
|
+
client.push( payload.as_json )
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
data/lib/parse/query.rb
ADDED
@@ -0,0 +1,378 @@
|
|
1
|
+
require_relative "client"
|
2
|
+
require_relative "query/operation"
|
3
|
+
require_relative "query/constraints"
|
4
|
+
require_relative "query/ordering"
|
5
|
+
|
6
|
+
|
7
|
+
module Parse
|
8
|
+
# This is the main engine behind making Parse queries on tables. It takes
|
9
|
+
# a set of constraints and generatse the proper hash parameters that are passed
|
10
|
+
# to a client :get request in order to retrive the results.
|
11
|
+
# The design of querying is based on ruby DataMapper orm where we define
|
12
|
+
# symbols with specific methos attached to values.
|
13
|
+
# At the core of each item is a Parse::Operation. An operation is
|
14
|
+
# made up of a field name and an operator. Therefore calling
|
15
|
+
# something like :name.eq, defines an equality operator on the field
|
16
|
+
# name. Using Parse::Operations with values, we can build different types of
|
17
|
+
# constraints - as Parse::Constraint
|
18
|
+
|
19
|
+
class Query
|
20
|
+
include Parse::Client::Connectable
|
21
|
+
# A query needs to be tied to a Parse table name (Parse class)
|
22
|
+
# The client object is of type Parse::Client in order to send query requests.
|
23
|
+
# You can modify the default client being used by all Parse::Query objects by setting
|
24
|
+
# Parse::Query.client. You can override individual Parse::Query object clients
|
25
|
+
# by changing their client variable to a different Parse::Client object.
|
26
|
+
attr_accessor :table, :client
|
27
|
+
|
28
|
+
# We have a special class method to handle field formatting. This turns
|
29
|
+
# the symbol keys in an operand from one key to another. For example, we can
|
30
|
+
# have the keys like :cost_rate in a query be translated to "costRate" when we
|
31
|
+
# build the query string before sending to Parse. This would allow you to have
|
32
|
+
# underscore case for your ruby code, while still maintaining camelCase in Parse.
|
33
|
+
# The default field formatter method is :columnize (which is camel case with the first letter
|
34
|
+
# in lower case). You can specify a different method to call by setting the Parse::Query.field_formatter
|
35
|
+
# variable with the symbol name of the method to call on the object. You can set this to nil
|
36
|
+
# if you do not want any field formatting to be performed.
|
37
|
+
class << self
|
38
|
+
#field formatter getters and setters.
|
39
|
+
attr_accessor :field_formatter
|
40
|
+
|
41
|
+
def field_formatter
|
42
|
+
#default
|
43
|
+
@field_formatter ||= :columnize
|
44
|
+
end
|
45
|
+
|
46
|
+
def format_field(str)
|
47
|
+
res = str.to_s
|
48
|
+
if field_formatter.present?
|
49
|
+
formatter = field_formatter.to_sym
|
50
|
+
# don't format if object d
|
51
|
+
res = res.send(formatter) if res.respond_to?(formatter)
|
52
|
+
end
|
53
|
+
res.strip
|
54
|
+
end
|
55
|
+
|
56
|
+
# Simple way to create a query.
|
57
|
+
def all(table, constraints = {})
|
58
|
+
self.new(table, {limit: :max}.merge(constraints) )
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
def client
|
64
|
+
# use the set client or the default client.
|
65
|
+
@client ||= self.class.client
|
66
|
+
end
|
67
|
+
|
68
|
+
def clear(item = :results)
|
69
|
+
case item
|
70
|
+
when :where
|
71
|
+
# an array of Parse::Constraint subclasses
|
72
|
+
@where = []
|
73
|
+
when :order
|
74
|
+
# an array of Parse::Order objects
|
75
|
+
@order = []
|
76
|
+
when :includes
|
77
|
+
@includes = []
|
78
|
+
when :skip
|
79
|
+
@skip = 0
|
80
|
+
when :limit
|
81
|
+
@limit = 100
|
82
|
+
when :count
|
83
|
+
@count = 0
|
84
|
+
when :keys
|
85
|
+
@keys = []
|
86
|
+
end
|
87
|
+
@results = nil
|
88
|
+
|
89
|
+
self
|
90
|
+
end
|
91
|
+
|
92
|
+
def initialize(table, constraints = {})
|
93
|
+
raise "First parameter should be the name of the Parse class (table)" unless table.is_a?(String)
|
94
|
+
@count = 0 #non-zero/1 implies a count query request
|
95
|
+
@where = []
|
96
|
+
@order = []
|
97
|
+
@keys = []
|
98
|
+
@includes = []
|
99
|
+
@limit = 100
|
100
|
+
@skip = 0
|
101
|
+
@table = table
|
102
|
+
conditions constraints
|
103
|
+
self # chaining
|
104
|
+
end # initialize
|
105
|
+
|
106
|
+
def conditions(expressions = {})
|
107
|
+
expressions.each do |expression, value|
|
108
|
+
if expression == :order
|
109
|
+
order value
|
110
|
+
elsif expression == :keys
|
111
|
+
keys value
|
112
|
+
elsif expression == :skip
|
113
|
+
skip value
|
114
|
+
elsif expression == :limit
|
115
|
+
limit value
|
116
|
+
elsif expression == :include || expression == :includes
|
117
|
+
includes(value)
|
118
|
+
else
|
119
|
+
add_constraint(expression, value)
|
120
|
+
end
|
121
|
+
end # each
|
122
|
+
end
|
123
|
+
|
124
|
+
def table=(t)
|
125
|
+
@table = t.to_s.camelize
|
126
|
+
end
|
127
|
+
|
128
|
+
# returns the query parameter for the particular clause
|
129
|
+
def clause(clause_name = :where)
|
130
|
+
return unless [:keys, :where, :order, :includes, :limit, :skip].include?(clause_name)
|
131
|
+
instance_variable_get "@#{clause_name}".to_sym
|
132
|
+
end
|
133
|
+
|
134
|
+
def keys(*fields)
|
135
|
+
@keys ||= []
|
136
|
+
fields.flatten.each do |field|
|
137
|
+
if field.nil? == false && field.respond_to?(:to_s)
|
138
|
+
@keys.push Query.format_field(field).to_sym
|
139
|
+
end
|
140
|
+
end
|
141
|
+
@keys.uniq!
|
142
|
+
@results = nil if fields.count > 0
|
143
|
+
self # chaining
|
144
|
+
end
|
145
|
+
|
146
|
+
def order(*ordering)
|
147
|
+
@order ||= []
|
148
|
+
ordering.flatten.each do |order|
|
149
|
+
order = Order.new(order) if order.respond_to?(:to_sym)
|
150
|
+
if order.is_a?(Order)
|
151
|
+
order.field = Query.format_field(order.field)
|
152
|
+
@order.push order
|
153
|
+
end
|
154
|
+
end #value.each
|
155
|
+
@results = nil if ordering.count > 0
|
156
|
+
self #chaining
|
157
|
+
end #order
|
158
|
+
|
159
|
+
def skip(count)
|
160
|
+
# min <= count <= max
|
161
|
+
@skip = [ 0, count.to_i, 10_000].sort[1]
|
162
|
+
@results = nil
|
163
|
+
self #chaining
|
164
|
+
end
|
165
|
+
|
166
|
+
def limit(count)
|
167
|
+
if count == :max || count == :all
|
168
|
+
@limit = 11_000
|
169
|
+
elsif count.is_a?(Numeric)
|
170
|
+
@limit = [ 0, count.to_i, 11_000].sort[1]
|
171
|
+
end
|
172
|
+
|
173
|
+
@results = nil
|
174
|
+
self #chaining
|
175
|
+
end
|
176
|
+
|
177
|
+
def related_to(field, pointer)
|
178
|
+
raise "Object value must be a Parse::Pointer type" unless pointer.is_a?(Parse::Pointer)
|
179
|
+
add_constraint field.to_sym.related_to, pointer
|
180
|
+
self
|
181
|
+
end
|
182
|
+
|
183
|
+
def includes(*fields)
|
184
|
+
@includes ||= []
|
185
|
+
fields.flatten.each do |field|
|
186
|
+
if field.nil? == false && field.respond_to?(:to_s)
|
187
|
+
@includes.push Query.format_field(field).to_sym
|
188
|
+
end
|
189
|
+
end
|
190
|
+
@includes.uniq!
|
191
|
+
@results = nil if fields.count > 0
|
192
|
+
self # chaining
|
193
|
+
end
|
194
|
+
alias_method :include, :includes
|
195
|
+
|
196
|
+
def add_constraint(operator, value, opts = {})
|
197
|
+
@where ||= []
|
198
|
+
constraint = Parse::Constraint.create operator, value
|
199
|
+
return unless constraint.is_a?(Parse::Constraint)
|
200
|
+
unless opts[:filter] == false
|
201
|
+
constraint.operand = Query.format_field(constraint.operand)
|
202
|
+
end
|
203
|
+
@where.push constraint
|
204
|
+
@results = nil
|
205
|
+
self #chaining
|
206
|
+
end
|
207
|
+
def constraints; @where; end;
|
208
|
+
|
209
|
+
def where(conditions = nil, opts = {})
|
210
|
+
return @where if conditions.nil?
|
211
|
+
if conditions.is_a?(Hash)
|
212
|
+
conditions.each do |operator, value|
|
213
|
+
add_constraint(operator, value, opts)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
self #chaining
|
217
|
+
end
|
218
|
+
|
219
|
+
def or_where(where_clauses = [])
|
220
|
+
where_clauses = where_clauses.where if where_clauses.is_a?(Parse::Query)
|
221
|
+
where_clauses = Parse::Query.new(@table, where_clauses ).where if where_clauses.is_a?(Hash)
|
222
|
+
return self if where_clauses.blank?
|
223
|
+
# we can only have one compound query constraint. If we need to add another OR clause
|
224
|
+
# let's find the one we have (if any)
|
225
|
+
compound = @where.find { |f| f.is_a?(Parse::CompoundQueryConstraint) }
|
226
|
+
# create a set of clauses that are not an OR clause.
|
227
|
+
remaining_clauses = @where.select { |f| f.is_a?(Parse::CompoundQueryConstraint) == false }
|
228
|
+
# if we don't have a OR clause to reuse, then create a new one with then
|
229
|
+
# current set of constraints
|
230
|
+
if compound.blank?
|
231
|
+
compound = Parse::CompoundQueryConstraint.new :or, [ Parse::Query.compile_where(remaining_clauses) ]
|
232
|
+
end
|
233
|
+
# then take the where clauses from the second query and append them.
|
234
|
+
compound.value.push Parse::Query.compile_where(where_clauses)
|
235
|
+
#compound = Parse::CompoundQueryConstraint.new :or, [remaining_clauses, or_where_query.where]
|
236
|
+
@where = [compound]
|
237
|
+
self #chaining
|
238
|
+
end
|
239
|
+
|
240
|
+
def |(other_query)
|
241
|
+
raise "Parse queries must be of the same class #{@table}." unless @table == other_query.table
|
242
|
+
copy_query = self.clone
|
243
|
+
copy_query.or_where other_query.where
|
244
|
+
copy_query
|
245
|
+
end
|
246
|
+
|
247
|
+
def count
|
248
|
+
@results = nil
|
249
|
+
old_value = @count
|
250
|
+
@count = 1
|
251
|
+
res = client.find_objects(@table, compile.as_json ).count
|
252
|
+
@count = old_value
|
253
|
+
res
|
254
|
+
end
|
255
|
+
|
256
|
+
def each
|
257
|
+
return results.enum_for(:each) unless block_given? # Sparkling magic!
|
258
|
+
results.each(&Proc.new)
|
259
|
+
end
|
260
|
+
|
261
|
+
def first(limit = 1)
|
262
|
+
@results = nil
|
263
|
+
@limit = limit
|
264
|
+
results.first(limit)
|
265
|
+
end
|
266
|
+
|
267
|
+
def max_results(raw: false)
|
268
|
+
compiled_query = compile
|
269
|
+
query_limit = compiled_query[:limit] ||= 1_000
|
270
|
+
query_skip = compiled_query[:skip] ||= 0
|
271
|
+
compiled_query[:limit] = 1_000
|
272
|
+
iterations = (query_limit/1000.0).ceil
|
273
|
+
results = []
|
274
|
+
|
275
|
+
iterations.times do |idx|
|
276
|
+
#puts "Fetching 1000 after #{compiled_query[:skip]}"
|
277
|
+
response = fetch!( compiled_query )
|
278
|
+
break if response.error? || response.results.empty?
|
279
|
+
#puts "Appending #{response.results.count} results..."
|
280
|
+
items = response.results
|
281
|
+
items = decode(items) unless raw
|
282
|
+
|
283
|
+
if block_given?
|
284
|
+
items.each(&Proc.new)
|
285
|
+
else
|
286
|
+
results += items
|
287
|
+
end
|
288
|
+
# if we get less than the maximum set of results, most likely the next
|
289
|
+
# query will return emtpy results - no need to perform it.
|
290
|
+
break if items.count < compiled_query[:limit]
|
291
|
+
# add to the skip count for the next iteration
|
292
|
+
compiled_query[:skip] += 1_000
|
293
|
+
break if compiled_query[:skip] > 10_000
|
294
|
+
end
|
295
|
+
results
|
296
|
+
end
|
297
|
+
|
298
|
+
def fetch!(compiled_query)
|
299
|
+
response = client.find_objects(@table, compiled_query.as_json )
|
300
|
+
if response.error?
|
301
|
+
puts "[ParseQuery] #{response.error}"
|
302
|
+
end
|
303
|
+
response
|
304
|
+
end
|
305
|
+
|
306
|
+
def results(raw: false)
|
307
|
+
if @results.nil?
|
308
|
+
if @limit <= 1_000
|
309
|
+
response = fetch!( compile )
|
310
|
+
return [] if response.error?
|
311
|
+
items = raw ? response.results : decode(response.results)
|
312
|
+
return items.each(&Proc.new) if block_given?
|
313
|
+
@results = items
|
314
|
+
elsif block_given?
|
315
|
+
return max_results(raw: raw, &Proc.new)
|
316
|
+
else
|
317
|
+
@results = max_results(raw: raw)
|
318
|
+
end
|
319
|
+
end
|
320
|
+
@results
|
321
|
+
end
|
322
|
+
alias_method :result, :results
|
323
|
+
|
324
|
+
def decode(list)
|
325
|
+
list.map { |m| Parse::Object.build(m, @table) }.compact
|
326
|
+
end
|
327
|
+
|
328
|
+
def as_json(*args)
|
329
|
+
compile.as_json
|
330
|
+
end
|
331
|
+
|
332
|
+
def compile(encode = true)
|
333
|
+
q = {} #query
|
334
|
+
q[:limit] = 11_000 if @limit == :max || @limit == :all
|
335
|
+
q[:limit] = @limit if @limit.is_a?(Numeric) && @limit > 0
|
336
|
+
q[:skip] = @skip if @skip > 0
|
337
|
+
|
338
|
+
q[:include] = @includes.join(',') unless @includes.empty?
|
339
|
+
q[:keys] = @keys.join(',') unless @keys.empty?
|
340
|
+
q[:order] = @order.join(',') unless @order.empty?
|
341
|
+
unless @where.empty?
|
342
|
+
q[:where] = Parse::Query.compile_where(@where)
|
343
|
+
q[:where] = q[:where].to_json if encode
|
344
|
+
end
|
345
|
+
|
346
|
+
if @count && @count > 0
|
347
|
+
# if count is requested
|
348
|
+
q[:limit] = 0
|
349
|
+
q[:count] = 1
|
350
|
+
end
|
351
|
+
q
|
352
|
+
end
|
353
|
+
|
354
|
+
def compile_where
|
355
|
+
self.class.compile_where( @where || [] )
|
356
|
+
end
|
357
|
+
|
358
|
+
def self.compile_where(where)
|
359
|
+
constraint_reduce( where )
|
360
|
+
end
|
361
|
+
|
362
|
+
def self.constraint_reduce(clauses)
|
363
|
+
# TODO: Need to add proper constraint merging
|
364
|
+
clauses.reduce({}) do |clause, subclause|
|
365
|
+
#puts "Merging Subclause: #{subclause.as_json}"
|
366
|
+
|
367
|
+
clause.deep_merge!( subclause.as_json || {} )
|
368
|
+
clause
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
def print
|
373
|
+
puts JSON.pretty_generate( as_json )
|
374
|
+
end
|
375
|
+
|
376
|
+
end # Query
|
377
|
+
|
378
|
+
end # Parse
|