parse-stack 1.7.3 → 1.9.1
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 +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
|