parse-stack 1.7.3 → 1.9.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.github/workflows/ruby.yml +36 -0
- data/.solargraph.yml +23 -0
- data/.travis.yml +6 -3
- data/Changes.md +84 -22
- data/Gemfile +14 -12
- data/Gemfile.lock +110 -60
- data/README.md +67 -24
- data/Rakefile +14 -14
- data/bin/parse-console +1 -0
- data/lib/parse/api/aggregate.rb +59 -0
- data/lib/parse/api/all.rb +2 -1
- data/lib/parse/api/analytics.rb +0 -3
- data/lib/parse/api/batch.rb +3 -5
- data/lib/parse/api/cloud_functions.rb +0 -3
- data/lib/parse/api/config.rb +0 -4
- data/lib/parse/api/files.rb +3 -7
- data/lib/parse/api/hooks.rb +4 -8
- data/lib/parse/api/objects.rb +9 -14
- data/lib/parse/api/push.rb +0 -4
- data/lib/parse/api/schema.rb +2 -6
- data/lib/parse/api/server.rb +4 -7
- data/lib/parse/api/sessions.rb +2 -5
- data/lib/parse/api/users.rb +9 -14
- data/lib/parse/client.rb +55 -50
- data/lib/parse/client/authentication.rb +29 -33
- data/lib/parse/client/batch.rb +8 -11
- data/lib/parse/client/body_builder.rb +19 -20
- data/lib/parse/client/caching.rb +23 -28
- data/lib/parse/client/protocol.rb +11 -12
- data/lib/parse/client/request.rb +4 -6
- data/lib/parse/client/response.rb +5 -7
- data/lib/parse/model/acl.rb +14 -12
- data/lib/parse/model/associations/belongs_to.rb +19 -24
- data/lib/parse/model/associations/collection_proxy.rb +328 -317
- data/lib/parse/model/associations/has_many.rb +22 -27
- data/lib/parse/model/associations/has_one.rb +7 -12
- data/lib/parse/model/associations/pointer_collection_proxy.rb +5 -13
- data/lib/parse/model/associations/relation_collection_proxy.rb +5 -9
- data/lib/parse/model/bytes.rb +8 -10
- data/lib/parse/model/classes/installation.rb +2 -4
- data/lib/parse/model/classes/product.rb +2 -5
- data/lib/parse/model/classes/role.rb +3 -5
- data/lib/parse/model/classes/session.rb +2 -5
- data/lib/parse/model/classes/user.rb +21 -17
- data/lib/parse/model/core/actions.rb +31 -46
- data/lib/parse/model/core/builder.rb +6 -6
- data/lib/parse/model/core/errors.rb +0 -1
- data/lib/parse/model/core/fetching.rb +45 -50
- data/lib/parse/model/core/properties.rb +53 -68
- data/lib/parse/model/core/querying.rb +292 -282
- data/lib/parse/model/core/schema.rb +89 -92
- data/lib/parse/model/date.rb +16 -23
- data/lib/parse/model/file.rb +171 -174
- data/lib/parse/model/geopoint.rb +12 -16
- data/lib/parse/model/model.rb +31 -37
- data/lib/parse/model/object.rb +58 -70
- data/lib/parse/model/pointer.rb +177 -176
- data/lib/parse/model/push.rb +8 -10
- data/lib/parse/model/shortnames.rb +1 -2
- data/lib/parse/model/time_zone.rb +3 -5
- data/lib/parse/query.rb +70 -37
- data/lib/parse/query/constraint.rb +4 -6
- data/lib/parse/query/constraints.rb +62 -20
- data/lib/parse/query/operation.rb +8 -11
- data/lib/parse/query/ordering.rb +45 -49
- data/lib/parse/stack.rb +15 -11
- data/lib/parse/stack/generators/rails.rb +28 -30
- data/lib/parse/stack/generators/templates/model.erb +5 -6
- data/lib/parse/stack/generators/templates/model_installation.rb +0 -1
- data/lib/parse/stack/generators/templates/model_role.rb +0 -1
- data/lib/parse/stack/generators/templates/model_session.rb +0 -1
- data/lib/parse/stack/generators/templates/model_user.rb +0 -1
- data/lib/parse/stack/generators/templates/parse.rb +9 -9
- data/lib/parse/stack/generators/templates/webhooks.rb +1 -2
- data/lib/parse/stack/railtie.rb +2 -4
- data/lib/parse/stack/tasks.rb +70 -86
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/webhooks.rb +19 -26
- data/lib/parse/webhooks/payload.rb +26 -28
- data/lib/parse/webhooks/registration.rb +23 -31
- data/parse-stack.gemspec +28 -28
- data/parse-stack.png +0 -0
- metadata +27 -25
- data/.github/parse-ruby-sdk.png +0 -0
@@ -1,18 +1,17 @@
|
|
1
1
|
# encoding: UTF-8
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
8
|
-
require
|
9
|
-
require
|
10
|
-
require
|
11
|
-
require
|
12
|
-
require
|
13
|
-
require
|
14
|
-
require
|
15
|
-
|
4
|
+
require "active_model"
|
5
|
+
require "active_support"
|
6
|
+
require "active_support/inflector"
|
7
|
+
require "active_support/core_ext"
|
8
|
+
require "active_support/core_ext/object"
|
9
|
+
require "active_support/inflector"
|
10
|
+
require "active_model_serializers"
|
11
|
+
require "active_support/inflector"
|
12
|
+
require "active_model_serializers"
|
13
|
+
require "active_support/hash_with_indifferent_access"
|
14
|
+
require "time"
|
16
15
|
|
17
16
|
module Parse
|
18
17
|
|
@@ -22,15 +21,15 @@ module Parse
|
|
22
21
|
# These are the base types supported by Parse.
|
23
22
|
TYPES = [:string, :relation, :integer, :float, :boolean, :date, :array, :file, :geopoint, :bytes, :object, :acl, :timezone].freeze
|
24
23
|
# These are the base mappings of the remote field name types.
|
25
|
-
BASE = {objectId: :string, createdAt: :date, updatedAt: :date, ACL: :acl}.freeze
|
24
|
+
BASE = { objectId: :string, createdAt: :date, updatedAt: :date, ACL: :acl }.freeze
|
26
25
|
# The list of properties that are part of all objects
|
27
26
|
BASE_KEYS = [:id, :created_at, :updated_at].freeze
|
28
27
|
# Default hash map of local attribute name to remote column name
|
29
|
-
BASE_FIELD_MAP = {id: :objectId, created_at: :createdAt, updated_at: :updatedAt, acl: :ACL}.freeze
|
28
|
+
BASE_FIELD_MAP = { id: :objectId, created_at: :createdAt, updated_at: :updatedAt, acl: :ACL }.freeze
|
30
29
|
# The delete operation hash.
|
31
|
-
CORE_FIELDS = {id: :string, created_at: :date, updated_at: :date, acl: :acl}.freeze
|
30
|
+
CORE_FIELDS = { id: :string, created_at: :date, updated_at: :date, acl: :acl }.freeze
|
32
31
|
# The delete operation hash.
|
33
|
-
DELETE_OP = {"__op"=>"Delete"}.freeze
|
32
|
+
DELETE_OP = { "__op" => "Delete" }.freeze
|
34
33
|
# @!visibility private
|
35
34
|
def self.included(base)
|
36
35
|
base.extend(ClassMethods)
|
@@ -49,7 +48,7 @@ module Parse
|
|
49
48
|
@fields ||= (self == Parse::Object ? CORE_FIELDS : Parse::Object.fields).dup
|
50
49
|
if type.present?
|
51
50
|
type = type.to_sym
|
52
|
-
return @fields.select { |k,v| v == type }
|
51
|
+
return @fields.select { |k, v| v == type }
|
53
52
|
end
|
54
53
|
@fields
|
55
54
|
end
|
@@ -101,7 +100,6 @@ module Parse
|
|
101
100
|
# "myDate" (lower-first-camelcase) with a Parse data type of Date.
|
102
101
|
# You can override the implicit naming behavior by passing the option :field to override.
|
103
102
|
def property(key, data_type = :string, **opts)
|
104
|
-
|
105
103
|
key = key.to_sym
|
106
104
|
ivar = :"@#{key}"
|
107
105
|
will_change_method = :"#{key}_will_change!"
|
@@ -125,25 +123,24 @@ module Parse
|
|
125
123
|
|
126
124
|
# set defaults
|
127
125
|
opts = { required: false,
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
}.merge( opts )
|
126
|
+
alias: true,
|
127
|
+
symbolize: false,
|
128
|
+
enum: nil,
|
129
|
+
scopes: true,
|
130
|
+
_prefix: nil,
|
131
|
+
_suffix: false,
|
132
|
+
field: key.to_s.camelize(:lower) }.merge(opts)
|
136
133
|
#By default, the remote field name is a lower-first-camelcase version of the key
|
137
134
|
# it can be overriden by the :field parameter
|
138
135
|
parse_field = opts[:field].to_sym
|
139
136
|
# if this is a custom property that is already defined, OR it is a subclass trying to define a core property
|
140
137
|
# then warn and exit.
|
141
|
-
if (self.fields[key].present? && BASE_FIELD_MAP[key].nil?) || (
|
138
|
+
if (self.fields[key].present? && BASE_FIELD_MAP[key].nil?) || (self < Parse::Object && BASE_FIELD_MAP.has_key?(key))
|
142
139
|
warn "Property #{self}##{key} already defined with data type :#{data_type}. Will be ignored."
|
143
140
|
return false
|
144
141
|
end
|
145
142
|
# We keep the list of fields that are on the remote Parse store
|
146
|
-
if self.fields[parse_field].present? || (
|
143
|
+
if self.fields[parse_field].present? || (self < Parse::Object && BASE.has_key?(parse_field))
|
147
144
|
warn "Alias property #{self}##{parse_field} conflicts with previously defined property. Will be ignored."
|
148
145
|
return false
|
149
146
|
# raise ArgumentError
|
@@ -152,14 +149,14 @@ module Parse
|
|
152
149
|
define_attribute_methods key
|
153
150
|
|
154
151
|
# this hash keeps list of attributes (based on remote fields) and their data types
|
155
|
-
self.attributes.merge!(
|
152
|
+
self.attributes.merge!(parse_field => data_type)
|
156
153
|
# this maps all the possible attribute fields and their data types. We use both local
|
157
154
|
# keys and remote keys because when we receive a remote object that has the remote field name
|
158
155
|
# we need to know what the data type conversion should be.
|
159
|
-
self.fields.merge!(
|
156
|
+
self.fields.merge!(key => data_type, parse_field => data_type)
|
160
157
|
# This creates a mapping between the local field and the remote field name.
|
161
|
-
self.field_map.merge!(
|
162
|
-
|
158
|
+
self.field_map.merge!(key => parse_field)
|
159
|
+
|
163
160
|
# if the field is marked as required, then add validations
|
164
161
|
if opts[:required]
|
165
162
|
# if integer or float, validate that it's a number
|
@@ -183,7 +180,6 @@ module Parse
|
|
183
180
|
is_enum_type = opts[:enum].nil? == false
|
184
181
|
|
185
182
|
if is_enum_type
|
186
|
-
|
187
183
|
unless data_type == :string
|
188
184
|
raise ArgumentError, "Property #{self}##{parse_field} :enum option is only supported on :string data types."
|
189
185
|
end
|
@@ -196,7 +192,7 @@ module Parse
|
|
196
192
|
|
197
193
|
enum_values = enum_values.dup.map(&:to_sym).freeze
|
198
194
|
|
199
|
-
self.enums.merge!(
|
195
|
+
self.enums.merge!(key => enum_values)
|
200
196
|
allow_nil = opts[:required] == false
|
201
197
|
validates key, inclusion: { in: enum_values }, allow_nil: allow_nil
|
202
198
|
|
@@ -217,9 +213,7 @@ module Parse
|
|
217
213
|
|
218
214
|
class_method_name = prefix_or_key.to_s.pluralize.to_sym
|
219
215
|
if singleton_class.method_defined?(class_method_name)
|
220
|
-
raise ArgumentError, "You tried to define an enum named `#{key}` for #{self} " +
|
221
|
-
"but this will generate a method `#{self}.#{class_method_name}` " + \
|
222
|
-
" which is already defined. Try using :_suffix or :_prefix options."
|
216
|
+
raise ArgumentError, "You tried to define an enum named `#{key}` for #{self} " + "but this will generate a method `#{self}.#{class_method_name}` " + " which is already defined. Try using :_suffix or :_prefix options."
|
223
217
|
end
|
224
218
|
|
225
219
|
define_singleton_method(class_method_name) { enum_values }
|
@@ -239,18 +233,14 @@ module Parse
|
|
239
233
|
elsif prefix.present?
|
240
234
|
method_name = :"#{prefix}_#{enum}"
|
241
235
|
end
|
242
|
-
self.scope method_name, ->(ex = {}){ ex.merge!(key => enum); query(
|
243
|
-
|
236
|
+
self.scope method_name, ->(ex = {}) { ex.merge!(key => enum); query(ex) }
|
244
237
|
|
245
238
|
define_method("#{method_name}!") { send set_attribute_method, enum, true }
|
246
239
|
define_method("#{method_name}?") { enum == send(key).to_s.to_sym }
|
247
240
|
end
|
248
241
|
end # unless scopes
|
249
|
-
|
250
242
|
end # if is enum
|
251
243
|
|
252
|
-
|
253
|
-
|
254
244
|
symbolize_value = opts[:symbolize]
|
255
245
|
|
256
246
|
#only support symbolization of string data types
|
@@ -271,7 +261,6 @@ module Parse
|
|
271
261
|
# we'll assume it's just a plain literal value
|
272
262
|
default_value.is_a?(Proc) ? default_value.call(self) : default_value
|
273
263
|
end
|
274
|
-
|
275
264
|
end
|
276
265
|
|
277
266
|
# We define a getter with the key
|
@@ -295,12 +284,11 @@ module Parse
|
|
295
284
|
# if value is nil (even after fetching), then lets see if the developer
|
296
285
|
# set a default value for this attribute.
|
297
286
|
if value.nil? && respond_to?("#{key}_default")
|
298
|
-
|
299
|
-
|
300
|
-
value = format_value(key, value, data_type)
|
287
|
+
value = send("#{key}_default")
|
288
|
+
value = format_value(key, value, data_type)
|
301
289
|
# lets set the variable with the updated value
|
302
|
-
|
303
|
-
|
290
|
+
instance_variable_set ivar, value
|
291
|
+
send will_change_method
|
304
292
|
elsif value.nil? && data_type == :array
|
305
293
|
value = Parse::CollectionProxy.new [], delegate: self, key: key
|
306
294
|
instance_variable_set ivar, value
|
@@ -318,12 +306,12 @@ module Parse
|
|
318
306
|
end
|
319
307
|
# finally return the value
|
320
308
|
if symbolize_value
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
309
|
+
if data_type == :string
|
310
|
+
return value.respond_to?(:to_sym) ? value.to_sym : value
|
311
|
+
elsif data_type == :array && value.is_a?(Array)
|
312
|
+
# value.map(&:to_sym)
|
313
|
+
return value.compact.map { |m| m.respond_to?(:to_sym) ? m.to_sym : m }
|
314
|
+
end
|
327
315
|
end
|
328
316
|
|
329
317
|
value
|
@@ -338,7 +326,7 @@ module Parse
|
|
338
326
|
# returns true if set to true, false otherwise
|
339
327
|
define_method("#{key}?") { (send(key) == true) }
|
340
328
|
unless opts[:scopes] == false
|
341
|
-
scope key, ->(opts = {}){ query(
|
329
|
+
scope key, ->(opts = {}) { query(opts.merge(key => true)) }
|
342
330
|
end
|
343
331
|
elsif data_type == :integer || data_type == :float
|
344
332
|
if self.method_defined?("#{key}_increment!")
|
@@ -359,7 +347,7 @@ module Parse
|
|
359
347
|
end
|
360
348
|
|
361
349
|
if self.method_defined?("#{key}_decrement!")
|
362
|
-
|
350
|
+
warn "Creating decrement helper :#{key}_decrement!. Will overwrite existing method #{self}##{key}_decrement!."
|
363
351
|
end
|
364
352
|
|
365
353
|
define_method("#{key}_decrement!") do |amount = -1|
|
@@ -369,7 +357,6 @@ module Parse
|
|
369
357
|
amount = -amount if amount > 0
|
370
358
|
send("#{key}_increment!", amount)
|
371
359
|
end
|
372
|
-
|
373
360
|
end
|
374
361
|
|
375
362
|
# The second method to be defined is a setter method. This is done by
|
@@ -393,7 +380,7 @@ module Parse
|
|
393
380
|
# this will grab the current value and keep a copy of it - but we only do this if
|
394
381
|
# the new value being set is different from the current value stored.
|
395
382
|
if track == true
|
396
|
-
send will_change_method unless val == instance_variable_get(
|
383
|
+
send will_change_method unless val == instance_variable_get(ivar)
|
397
384
|
end
|
398
385
|
|
399
386
|
if symbolize_value
|
@@ -435,7 +422,6 @@ module Parse
|
|
435
422
|
end
|
436
423
|
true
|
437
424
|
end # property
|
438
|
-
|
439
425
|
end #ClassMethods
|
440
426
|
|
441
427
|
# @return [Hash] a hash mapping of all property fields and their types.
|
@@ -451,7 +437,7 @@ module Parse
|
|
451
437
|
# TODO: We can optimize
|
452
438
|
# @return [Hash] returns the list of property attributes for this class.
|
453
439
|
def attributes
|
454
|
-
{__type: :string, :className => :string}.merge!(self.class.attributes)
|
440
|
+
{ __type: :string, :className => :string }.merge!(self.class.attributes)
|
455
441
|
end
|
456
442
|
|
457
443
|
# support for setting a hash of attributes on the object with a given dirty tracking value
|
@@ -466,7 +452,7 @@ module Parse
|
|
466
452
|
@id ||= hash[Parse::Model::ID] || hash[Parse::Model::OBJECT_ID] || hash[:objectId]
|
467
453
|
hash.each do |key, value|
|
468
454
|
method = "#{key}_set_attribute!".freeze
|
469
|
-
send(method, value, dirty_track) if respond_to?(
|
455
|
+
send(method, value, dirty_track) if respond_to?(method)
|
470
456
|
end
|
471
457
|
end
|
472
458
|
|
@@ -494,7 +480,7 @@ module Parse
|
|
494
480
|
next unless fields[key].present?
|
495
481
|
remote_field = self.field_map[key] || key
|
496
482
|
h[remote_field] = send key
|
497
|
-
h[remote_field] = {__op: :Delete} if h[remote_field].nil?
|
483
|
+
h[remote_field] = { __op: :Delete } if h[remote_field].nil?
|
498
484
|
# in the case that the field is a Parse object, generate a pointer
|
499
485
|
# if it is a Parse::PointerCollectionProxy, then make sure we get a list of pointers.
|
500
486
|
h[remote_field] = h[remote_field].parse_pointers if h[remote_field].is_a?(Parse::PointerCollectionProxy)
|
@@ -509,6 +495,7 @@ module Parse
|
|
509
495
|
fields[key.to_sym].present?
|
510
496
|
end
|
511
497
|
end
|
498
|
+
|
512
499
|
# Returns a formatted value based on the operation hash and data_type of the property.
|
513
500
|
# For some values in Parse, they are specified as operation hashes which could include
|
514
501
|
# Add, Remove, Delete, AddUnique and Increment.
|
@@ -585,7 +572,7 @@ module Parse
|
|
585
572
|
val = val.to_f unless val.blank?
|
586
573
|
when :acl
|
587
574
|
# ACL types go through a special conversion
|
588
|
-
|
575
|
+
val = ACL.typecast(val, self)
|
589
576
|
when :date
|
590
577
|
# if it respond to parse_date, then use that as the conversion.
|
591
578
|
if val.respond_to?(:parse_date) && val.is_a?(Parse::Date) == false
|
@@ -596,9 +583,9 @@ module Parse
|
|
596
583
|
elsif val.is_a?(String)
|
597
584
|
# if it's a string, try parsing the date
|
598
585
|
val = Parse::Date.parse val
|
599
|
-
|
600
|
-
|
601
|
-
|
586
|
+
#elsif val.present?
|
587
|
+
# pus "[Parse::Stack] Invalid date value '#{val}' assigned to #{self.class}##{key}, it should be a Parse::Date or DateTime."
|
588
|
+
# raise ValueError, "Invalid date value '#{val}' assigned to #{self.class}##{key}, it should be a Parse::Date or DateTime."
|
602
589
|
end
|
603
590
|
when :timezone
|
604
591
|
val = Parse::TimeZone.new(val) if val.present?
|
@@ -613,7 +600,5 @@ module Parse
|
|
613
600
|
end
|
614
601
|
val
|
615
602
|
end
|
616
|
-
|
617
603
|
end # Properties
|
618
|
-
|
619
604
|
end # Parse
|
@@ -1,320 +1,330 @@
|
|
1
1
|
# encoding: UTF-8
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
require_relative
|
4
|
+
require_relative "../../query"
|
5
5
|
|
6
6
|
module Parse
|
7
7
|
module Core
|
8
8
|
# Defines the querying methods applied to a Parse::Object.
|
9
9
|
module Querying
|
10
10
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
name = name.to_sym
|
70
|
-
if respond_to?(name, true)
|
71
|
-
puts "Creating scope :#{name}. Will overwrite existing method #{self}.#{name}."
|
72
|
-
end
|
11
|
+
# This feature is a small subset of the
|
12
|
+
# {http://guides.rubyonrails.org/active_record_querying.html#scopes
|
13
|
+
# ActiveRecord named scopes} feature. Scoping allows you to specify
|
14
|
+
# commonly-used queries which can be referenced as class method calls and
|
15
|
+
# are chainable with other scopes. You can use every {Parse::Query}
|
16
|
+
# method previously covered such as `where`, `includes` and `limit`.
|
17
|
+
#
|
18
|
+
# class Article < Parse::Object
|
19
|
+
# property :published, :boolean
|
20
|
+
# scope :published, -> { query(published: true) }
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# This is the same as defining your own class method for the query.
|
24
|
+
#
|
25
|
+
# class Article < Parse::Object
|
26
|
+
# def self.published
|
27
|
+
# query(published: true)
|
28
|
+
# end
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# You can also chain scopes and pass parameters. In addition, boolean and
|
32
|
+
# enumerated properties have automatically generated scopes for you to use.
|
33
|
+
#
|
34
|
+
# class Article < Parse::Object
|
35
|
+
# scope :published, -> { query(published: true) }
|
36
|
+
#
|
37
|
+
# property :comment_count, :integer
|
38
|
+
# property :category
|
39
|
+
# property :approved, :boolean
|
40
|
+
#
|
41
|
+
# scope :published_and_commented, -> { published.where :comment_count.gt => 0 }
|
42
|
+
# scope :popular_topics, ->(name) { published_and_commented.where category: name }
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# # simple scope
|
46
|
+
# Article.published # => where published is true
|
47
|
+
#
|
48
|
+
# # chained scope
|
49
|
+
# Article.published_and_commented # published is true and comment_count > 0
|
50
|
+
#
|
51
|
+
# # scope with parameters
|
52
|
+
# Article.popular_topic("music") # => popular music articles
|
53
|
+
# # equivalent: where(published: true, :comment_count.gt => 0, category: name)
|
54
|
+
#
|
55
|
+
# # automatically generated scope
|
56
|
+
# Article.approved(category: "tour") # => where approved: true, category: 'tour'
|
57
|
+
#
|
58
|
+
# If you would like to turn off automatic scope generation for property types,
|
59
|
+
# set the option `:scope` to false when declaring the property.
|
60
|
+
# @param name [Symbol] the name of the scope.
|
61
|
+
# @param body [Proc] the proc related to the scope.
|
62
|
+
# @raise ArgumentError if body parameter does not respond to `call`
|
63
|
+
# @return [Symbol] the name of the singleton method created.
|
64
|
+
def scope(name, body)
|
65
|
+
unless body.respond_to?(:call)
|
66
|
+
raise ArgumentError, "The scope body needs to be callable."
|
67
|
+
end
|
73
68
|
|
74
|
-
|
69
|
+
name = name.to_sym
|
70
|
+
if respond_to?(name, true)
|
71
|
+
puts "Creating scope :#{name}. Will overwrite existing method #{self}.#{name}."
|
72
|
+
end
|
75
73
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
74
|
+
define_singleton_method(name) do |*args, &block|
|
75
|
+
if body.arity.zero?
|
76
|
+
res = body.call
|
77
|
+
res.conditions(*args) if args.present?
|
78
|
+
else
|
79
|
+
res = body.call(*args)
|
80
|
+
end
|
82
81
|
|
83
|
-
|
82
|
+
_q = res || query
|
84
83
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
klass = nil # help clean up ruby gc
|
98
|
-
return klass_scope
|
99
|
-
end
|
84
|
+
if _q.is_a?(Parse::Query)
|
85
|
+
klass = self
|
86
|
+
_q.define_singleton_method(:method_missing) do |m, *args, &chained_block|
|
87
|
+
if klass.respond_to?(m, true)
|
88
|
+
# must be a scope
|
89
|
+
klass_scope = klass.send(m, *args)
|
90
|
+
if klass_scope.is_a?(Parse::Query)
|
91
|
+
# merge constraints
|
92
|
+
add_constraints(klass_scope.constraints)
|
93
|
+
# if a block was passed, execute the query, otherwise return the query
|
94
|
+
return chained_block.present? ? results(&chained_block) : self
|
95
|
+
end # if
|
100
96
|
klass = nil # help clean up ruby gc
|
101
|
-
return
|
97
|
+
return klass_scope
|
102
98
|
end
|
99
|
+
klass = nil # help clean up ruby gc
|
100
|
+
return results.send(m, *args, &chained_block)
|
103
101
|
end
|
104
|
-
|
105
|
-
Parse::Query.apply_auto_introspection!(_q)
|
106
|
-
|
107
|
-
return _q if block.nil?
|
108
|
-
_q.results(&block)
|
109
102
|
end
|
110
103
|
|
111
|
-
|
104
|
+
Parse::Query.apply_auto_introspection!(_q)
|
112
105
|
|
113
|
-
|
114
|
-
|
115
|
-
# @example
|
116
|
-
# # assume Post < Parse::Object
|
117
|
-
# query = Post.query(:updated_at.before => DateTime.now)
|
118
|
-
# @return [Parse::Query] a new query with the given constraints for this
|
119
|
-
# Parse::Object subclass.
|
120
|
-
def query(constraints = {})
|
121
|
-
Parse::Query.new self.parse_class, constraints
|
122
|
-
end; alias_method :where, :query
|
123
|
-
|
124
|
-
# @param conditions (see Parse::Query#where)
|
125
|
-
# @return (see Parse::Query#where)
|
126
|
-
# @see Parse::Query#where
|
127
|
-
def literal_where(conditions = {})
|
128
|
-
query.where(conditions)
|
106
|
+
return _q if block.nil?
|
107
|
+
_q.results(&block)
|
129
108
|
end
|
109
|
+
end
|
130
110
|
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
#
|
142
|
-
# post = Post.first
|
143
|
-
# # iterate over all comments matching conditions
|
144
|
-
# Comment.each(post: post) do |comment|
|
145
|
-
# # ...
|
146
|
-
# end
|
147
|
-
# @return [Parse::Object] the last Parse::Object record processed.
|
148
|
-
# @note You cannot use *:created_at* as a constraint.
|
149
|
-
# @raise ArgumentError if :created_at is detected in the constraints argument.
|
150
|
-
# @see Parse::Core::Querying.all
|
151
|
-
# @see Parse::Core::Actions.save_all
|
152
|
-
def each(constraints = {}, &block)
|
153
|
-
# verify we don't hvae created at as a constraint, otherwise this will not work
|
154
|
-
invalid_constraints = constraints.keys.any? do |k|
|
155
|
-
(k == :created_at || k == :createdAt) ||
|
156
|
-
( k.is_a?(Parse::Operation) && (k.operand == :created_at || k.operand == :createdAt) )
|
157
|
-
end
|
158
|
-
if invalid_constraints
|
159
|
-
raise ArgumentError, "[#{self.class}.each] Special method each()" \
|
160
|
-
"cannot be used with a :created_at constraint."
|
161
|
-
end
|
162
|
-
batch_size = 250
|
163
|
-
start_cursor = first( order: :created_at.asc, keys: :created_at )
|
164
|
-
constraints.merge! cache: false, limit: batch_size, order: :created_at.asc
|
165
|
-
all_query = query(constraints)
|
166
|
-
cursor = start_cursor
|
167
|
-
# the exclusion set is a set of ids not to include the next query.
|
168
|
-
exclusion_set = []
|
169
|
-
loop do
|
170
|
-
_q = query(constraints.dup)
|
171
|
-
_q.where(:created_at.on_or_after => cursor.created_at)
|
172
|
-
# set of ids not to include in the next query. non-performant, but accurate.
|
173
|
-
_q.where(:id.nin => exclusion_set) unless exclusion_set.empty?
|
174
|
-
results = _q.results # get results
|
111
|
+
# Creates a new {Parse::Query} with the given constraints for this class.
|
112
|
+
# @example
|
113
|
+
# # assume Post < Parse::Object
|
114
|
+
# query = Post.query(:updated_at.before => DateTime.now)
|
115
|
+
# @return [Parse::Query] a new query with the given constraints for this
|
116
|
+
# Parse::Object subclass.
|
117
|
+
def query(constraints = {})
|
118
|
+
Parse::Query.new self.parse_class, constraints
|
119
|
+
end;
|
120
|
+
alias_method :where, :query
|
175
121
|
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
break next_cursor if cursor.id == next_cursor.id
|
183
|
-
# The exclusion set is used in the case where multiple records have the exact
|
184
|
-
# same created_at date (down to the microsecond). This prevents getting the same
|
185
|
-
# record in the next query request.
|
186
|
-
exclusion_set = results.select { |r| r.created_at == next_cursor.created_at }.map(&:id)
|
187
|
-
results = nil
|
188
|
-
cursor = next_cursor
|
189
|
-
end
|
122
|
+
# @param conditions (see Parse::Query#where)
|
123
|
+
# @return (see Parse::Query#where)
|
124
|
+
# @see Parse::Query#where
|
125
|
+
def literal_where(conditions = {})
|
126
|
+
query.where(conditions)
|
127
|
+
end
|
190
128
|
|
129
|
+
# This methods allow you to efficiently iterate over all the records in the collection
|
130
|
+
# (lower memory cost) at a minor cost of performance. This method utilizes
|
131
|
+
# the `created_at` field of Parse records to order and iterate over *all* matching records,
|
132
|
+
# therefore you should not use this method if you want to perform a query
|
133
|
+
# with constraints against the `created_at` field or need specific type of ordering.
|
134
|
+
# If you need to use `:created_at` in your constraints, consider using {Parse::Core::Querying#all} or
|
135
|
+
# {Parse::Core::Actions::ClassMethods#save_all}
|
136
|
+
# @param constraints [Hash] a set of query constraints.
|
137
|
+
# @yield a block which will iterate through each matching record.
|
138
|
+
# @example
|
139
|
+
#
|
140
|
+
# post = Post.first
|
141
|
+
# # iterate over all comments matching conditions
|
142
|
+
# Comment.each(post: post) do |comment|
|
143
|
+
# # ...
|
144
|
+
# end
|
145
|
+
# @return [Parse::Object] the last Parse::Object record processed.
|
146
|
+
# @note You cannot use *:created_at* as a constraint.
|
147
|
+
# @raise ArgumentError if :created_at is detected in the constraints argument.
|
148
|
+
# @see Parse::Core::Querying.all
|
149
|
+
# @see Parse::Core::Actions.save_all
|
150
|
+
def each(constraints = {}, &block)
|
151
|
+
# verify we don't hvae created at as a constraint, otherwise this will not work
|
152
|
+
invalid_constraints = constraints.keys.any? do |k|
|
153
|
+
(k == :created_at || k == :createdAt) ||
|
154
|
+
(k.is_a?(Parse::Operation) && (k.operand == :created_at || k.operand == :createdAt))
|
191
155
|
end
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
# When no block is passed, all objects are returned. Using a block is more memory
|
196
|
-
# efficient as matching objects are fetched in batches and discarded after the iteration
|
197
|
-
# is completed.
|
198
|
-
# @param constraints [Hash] a set of {Parse::Query} constraints.
|
199
|
-
# @yield a block to iterate with each matching object.
|
200
|
-
# @example
|
201
|
-
#
|
202
|
-
# songs = Song.all( ... expressions ...) # => array of Parse::Objects
|
203
|
-
# # memory efficient for large amounts of records.
|
204
|
-
# Song.all( ... expressions ...) do |song|
|
205
|
-
# # ... do something with song..
|
206
|
-
# end
|
207
|
-
#
|
208
|
-
# @note This method will continually query for records by automatically
|
209
|
-
# incrementing the *:skip* parameter until no more results are returned
|
210
|
-
# by the server.
|
211
|
-
# @return [Array<Parse::Object>] an array of matching objects. If a block is passed,
|
212
|
-
# an empty array is returned.
|
213
|
-
def all(constraints = {limit: :max})
|
214
|
-
constraints = constraints.reverse_merge({limit: :max})
|
215
|
-
prepared_query = query(constraints)
|
216
|
-
return prepared_query.results(&Proc.new) if block_given?
|
217
|
-
prepared_query.results
|
156
|
+
if invalid_constraints
|
157
|
+
raise ArgumentError, "[#{self.class}.each] Special method each()" \
|
158
|
+
"cannot be used with a :created_at constraint."
|
218
159
|
end
|
160
|
+
batch_size = 250
|
161
|
+
start_cursor = first(order: :created_at.asc, keys: :created_at)
|
162
|
+
constraints.merge! cache: false, limit: batch_size, order: :created_at.asc
|
163
|
+
all_query = query(constraints)
|
164
|
+
cursor = start_cursor
|
165
|
+
# the exclusion set is a set of ids not to include the next query.
|
166
|
+
exclusion_set = []
|
167
|
+
loop do
|
168
|
+
_q = query(constraints.dup)
|
169
|
+
_q.where(:created_at.on_or_after => cursor.created_at)
|
170
|
+
# set of ids not to include in the next query. non-performant, but accurate.
|
171
|
+
_q.where(:id.nin => exclusion_set) unless exclusion_set.empty?
|
172
|
+
results = _q.results # get results
|
219
173
|
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
fetch_count = 1
|
234
|
-
if constraints.is_a?(Numeric)
|
235
|
-
fetch_count = constraints.to_i
|
236
|
-
constraints = {}
|
237
|
-
end
|
238
|
-
constraints.merge!( {limit: fetch_count} )
|
239
|
-
res = query(constraints).results
|
240
|
-
return res.first if fetch_count == 1
|
241
|
-
return res.first fetch_count
|
174
|
+
break cursor if results.empty? # break if no results
|
175
|
+
results.each(&block)
|
176
|
+
next_cursor = results.last
|
177
|
+
# break if we got less than the maximum requested
|
178
|
+
break next_cursor if results.count < batch_size
|
179
|
+
# break if the next object is the same as the current object.
|
180
|
+
break next_cursor if cursor.id == next_cursor.id
|
181
|
+
# The exclusion set is used in the case where multiple records have the exact
|
182
|
+
# same created_at date (down to the microsecond). This prevents getting the same
|
183
|
+
# record in the next query request.
|
184
|
+
exclusion_set = results.select { |r| r.created_at == next_cursor.created_at }.map(&:id)
|
185
|
+
results = nil
|
186
|
+
cursor = next_cursor
|
242
187
|
end
|
188
|
+
end
|
243
189
|
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
190
|
+
# Fetch all matching objects in this collection matching the constraints.
|
191
|
+
# This will be the most common way when querying Parse objects for a subclass.
|
192
|
+
# When no block is passed, all objects are returned. Using a block is more memory
|
193
|
+
# efficient as matching objects are fetched in batches and discarded after the iteration
|
194
|
+
# is completed.
|
195
|
+
# @param constraints [Hash] a set of {Parse::Query} constraints.
|
196
|
+
# @yield a block to iterate with each matching object.
|
197
|
+
# @example
|
198
|
+
#
|
199
|
+
# songs = Song.all( ... expressions ...) # => array of Parse::Objects
|
200
|
+
# # memory efficient for large amounts of records.
|
201
|
+
# Song.all( ... expressions ...) do |song|
|
202
|
+
# # ... do something with song..
|
203
|
+
# end
|
204
|
+
#
|
205
|
+
# @note This method will continually query for records by automatically
|
206
|
+
# incrementing the *:skip* parameter until no more results are returned
|
207
|
+
# by the server.
|
208
|
+
# @return [Array<Parse::Object>] an array of matching objects. If a block is passed,
|
209
|
+
# an empty array is returned.
|
210
|
+
def all(constraints = { limit: :max })
|
211
|
+
constraints = constraints.reverse_merge({ limit: :max })
|
212
|
+
prepared_query = query(constraints)
|
213
|
+
return prepared_query.results(&Proc.new) if block_given?
|
214
|
+
prepared_query.results
|
215
|
+
end
|
254
216
|
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
217
|
+
# Returns the first item matching the constraint.
|
218
|
+
# @overload first(count = 1)
|
219
|
+
# @param count [Interger] The number of items to return.
|
220
|
+
# @example
|
221
|
+
# Object.first(2) # => an array of the first 2 objects in the collection.
|
222
|
+
# @return [Parse::Object] if count == 1
|
223
|
+
# @return [Array<Parse::Object>] if count > 1
|
224
|
+
# @overload first(constraints = {})
|
225
|
+
# @param constraints [Hash] a set of {Parse::Query} constraints.
|
226
|
+
# @example
|
227
|
+
# Object.first( :name => "Anthony" )
|
228
|
+
# @return [Parse::Object] the first matching object.
|
229
|
+
def first(constraints = {})
|
230
|
+
fetch_count = 1
|
231
|
+
if constraints.is_a?(Numeric)
|
232
|
+
fetch_count = constraints.to_i
|
233
|
+
constraints = {}
|
263
234
|
end
|
235
|
+
constraints.merge!({ limit: fetch_count })
|
236
|
+
res = query(constraints).results
|
237
|
+
return res.first if fetch_count == 1
|
238
|
+
return res.first fetch_count
|
239
|
+
end
|
264
240
|
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
241
|
+
# Creates a count request which is more performant when counting objects.
|
242
|
+
# @example
|
243
|
+
# # number of songs with a like count greater than 20.
|
244
|
+
# count = Song.count( :like_count.gt => 20 )
|
245
|
+
# @param constraints (see #all)
|
246
|
+
# @return [Interger] the number of records matching the query.
|
247
|
+
# @see Parse::Query#count
|
248
|
+
def count(constraints = {})
|
249
|
+
query(constraints).count
|
250
|
+
end
|
274
251
|
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
# @return [Parse::Object] if only one id was provided as a parameter.
|
288
|
-
# @return [Array<Parse::Object>] if more than one id was provided as a parameter.
|
289
|
-
def find(*parse_ids, type: :parallel, compact: true)
|
290
|
-
# flatten the list of Object ids.
|
291
|
-
parse_ids.flatten!
|
292
|
-
parse_ids.compact!
|
293
|
-
# determines if the result back to the call site is an array or a single result
|
294
|
-
as_array = parse_ids.count > 1
|
295
|
-
results = []
|
252
|
+
# Finds the distinct values for a specified field across a single
|
253
|
+
# collection or view and returns the results in an array.
|
254
|
+
# @example
|
255
|
+
# # get a list of unique city names for users who are older than 21.
|
256
|
+
# cities = User.distinct(:city, :age.gt => 21 )
|
257
|
+
# @param field The name of the field to use for unique aggregation.
|
258
|
+
# @param constraints (see #all)
|
259
|
+
# @return [Array] a list of distinct values
|
260
|
+
# @see Parse::Query#distinct
|
261
|
+
def distinct(field, constraints = {})
|
262
|
+
query(constraints).distinct(field)
|
263
|
+
end
|
296
264
|
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
265
|
+
# Find objects matching the constraint ordered by the descending created_at date.
|
266
|
+
# @param constraints (see #all)
|
267
|
+
# @return [Array<Parse::Object>]
|
268
|
+
def newest(constraints = {})
|
269
|
+
constraints.merge!(order: :created_at.desc)
|
270
|
+
_q = query(constraints)
|
271
|
+
_q.define_singleton_method(:method_missing) { |m, *args, &block| self.results.send(m, *args, &block) }
|
272
|
+
_q
|
273
|
+
end
|
274
|
+
|
275
|
+
# Find objects matching the constraint ordered by the ascending created_at date.
|
276
|
+
# @param constraints (see #all)
|
277
|
+
# @return [Array<Parse::Object>]
|
278
|
+
def oldest(constraints = {})
|
279
|
+
constraints.merge!(order: :created_at.asc)
|
280
|
+
_q = query(constraints)
|
281
|
+
_q.define_singleton_method(:method_missing) { |m, *args, &block| self.results.send(m, *args, &block) }
|
282
|
+
_q
|
283
|
+
end
|
314
284
|
|
315
|
-
|
316
|
-
|
285
|
+
# Find objects for a given objectId in this collection.The result is a list
|
286
|
+
# (or single item) of the objects that were successfully found.
|
287
|
+
# @example
|
288
|
+
# Object.find "<objectId>"
|
289
|
+
# Object.find "<objectId>", "<objectId>"....
|
290
|
+
# Object.find ["<objectId>", "<objectId>"]
|
291
|
+
# @param parse_ids [String] the objectId to find.
|
292
|
+
# @param type [Symbol] the fetching methodology to use if more than one id was passed.
|
293
|
+
# - *:parallel* : Utilizes parrallel HTTP requests to fetch all objects requested.
|
294
|
+
# - *:batch* : This uses a batch fetch request using a contained_in clause.
|
295
|
+
# @param compact [Boolean] whether to remove nil items from the returned array for objects
|
296
|
+
# that were not found.
|
297
|
+
# @return [Parse::Object] if only one id was provided as a parameter.
|
298
|
+
# @return [Array<Parse::Object>] if more than one id was provided as a parameter.
|
299
|
+
def find(*parse_ids, type: :parallel, compact: true)
|
300
|
+
# flatten the list of Object ids.
|
301
|
+
parse_ids.flatten!
|
302
|
+
parse_ids.compact!
|
303
|
+
# determines if the result back to the call site is an array or a single result
|
304
|
+
as_array = parse_ids.count > 1
|
305
|
+
results = []
|
306
|
+
|
307
|
+
if type == :batch
|
308
|
+
# use a .in query with the given id as a list
|
309
|
+
results = self.class.all(:id.in => parse_ids)
|
310
|
+
else
|
311
|
+
# use Parallel to make multiple threaded requests for finding these objects.
|
312
|
+
# The benefit of using this as default is that each request goes to a specific URL
|
313
|
+
# which is better than Query request (table scan). This in turn allows for caching of
|
314
|
+
# individual objects.
|
315
|
+
results = parse_ids.threaded_map do |parse_id|
|
316
|
+
next nil unless parse_id.present?
|
317
|
+
response = client.fetch_object(parse_class, parse_id)
|
318
|
+
next nil if response.error?
|
319
|
+
Parse::Object.build response.result, parse_class
|
320
|
+
end
|
321
|
+
end
|
322
|
+
# removes any nil items in the array
|
323
|
+
results.compact! if compact
|
317
324
|
|
325
|
+
as_array ? results : results.first
|
326
|
+
end;
|
327
|
+
alias_method :get, :find
|
318
328
|
end # Querying
|
319
329
|
end
|
320
330
|
end
|