parse-stack-next 5.7.0 → 5.7.2

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.
@@ -1561,7 +1561,6 @@ module Parse
1561
1561
 
1562
1562
  # if it's a hash, then it should be {:key=>"objectId", :query=>[]}
1563
1563
  remote_field_name = @operation.operand
1564
- query = nil
1565
1564
  if @value.is_a?(Hash)
1566
1565
  res = @value.symbolize_keys
1567
1566
  remote_field_name = res[:key] || remote_field_name
@@ -1613,7 +1612,6 @@ module Parse
1613
1612
 
1614
1613
  # if it's a hash, then it should be {:key=>"objectId", :query=>[]}
1615
1614
  remote_field_name = @operation.operand
1616
- query = nil
1617
1615
  if @value.is_a?(Hash)
1618
1616
  res = @value.symbolize_keys
1619
1617
  remote_field_name = res[:key] || remote_field_name
@@ -2263,7 +2261,6 @@ module Parse
2263
2261
  # @return [Hash] the compiled constraint.
2264
2262
  def build
2265
2263
  remote_field_name = @operation.operand
2266
- query = nil
2267
2264
 
2268
2265
  if @value.is_a?(Hash)
2269
2266
  res = @value.symbolize_keys
@@ -2314,7 +2311,6 @@ module Parse
2314
2311
  # @return [Hash] the compiled constraint.
2315
2312
  def build
2316
2313
  remote_field_name = @operation.operand
2317
- query = nil
2318
2314
 
2319
2315
  if @value.is_a?(Hash)
2320
2316
  res = @value.symbolize_keys
@@ -2516,6 +2512,19 @@ module Parse
2516
2512
  # User.where(:name.between => ["Alice", "John"])
2517
2513
  # # Generates: "name": { "$gte": "Alice", "$lte": "John" }
2518
2514
  #
2515
+ # # A Ruby Range works the same way as a 2-element array. An inclusive
2516
+ # # range (`..`) maps its end to $lte, while an exclusive range (`...`)
2517
+ # # maps its end to $lt.
2518
+ # User.where(:age.between => 18..65)
2519
+ # # Generates: "age": { "$gte": 18, "$lte": 65 }
2520
+ #
2521
+ # Record.where(:date.between => 5.days.ago...2.days.ago)
2522
+ # # Generates: "date": { "$gte": <5 days ago>, "$lt": <2 days ago> }
2523
+ #
2524
+ # # Beginless/endless ranges only constrain the side that is present.
2525
+ # User.where(:age.between => 18..)
2526
+ # # Generates: "age": { "$gte": 18 }
2527
+ #
2519
2528
  class BetweenConstraint < Constraint
2520
2529
  # @!method between
2521
2530
  # A registered method on a symbol to create the constraint.
@@ -2526,9 +2535,11 @@ module Parse
2526
2535
 
2527
2536
  # @return [Hash] the compiled constraint.
2528
2537
  def build
2538
+ return build_range(@value) if @value.is_a?(Range)
2539
+
2529
2540
  value = formatted_value
2530
2541
  unless value.is_a?(Array) && value.length == 2
2531
- raise ArgumentError, "#{self.class}: Value must be an array with exactly 2 elements [min_value, max_value]"
2542
+ raise ArgumentError, "#{self.class}: Value must be an array with exactly 2 elements [min_value, max_value], or a Range"
2532
2543
  end
2533
2544
 
2534
2545
  min_value, max_value = value
@@ -2542,6 +2553,51 @@ module Parse
2542
2553
  Parse::Constraint::LessThanOrEqualConstraint.key => formatted_max,
2543
2554
  } }
2544
2555
  end
2556
+
2557
+ # @!visibility private
2558
+ # Extract raw (unformatted) `[min_value, max_value, exclude_max]`
2559
+ # bounds from a `between`-style value: a Range (`exclude_max`
2560
+ # reflects `exclude_end?`) or a 2-element Array (always inclusive
2561
+ # on both ends, matching {#build}'s array branch). Shared with
2562
+ # {Parse::Query#where_not_between}, which needs the same bounds
2563
+ # to build the negated (`$or` of the two flipped comparisons)
2564
+ # form — a shape {BetweenConstraint} itself cannot safely emit
2565
+ # from a single constraint's `#build` (see `where_not_between`'s
2566
+ # docs for why).
2567
+ # @param value [Range, Array] a `between`-style value.
2568
+ # @return [Array(Object, Object, Boolean)]
2569
+ # @raise [ArgumentError] if `value` isn't a Range or 2-element Array.
2570
+ def self.extract_bounds(value)
2571
+ if value.is_a?(Range)
2572
+ [value.begin, value.end, value.exclude_end?]
2573
+ elsif value.is_a?(Array) && value.length == 2
2574
+ [value[0], value[1], false]
2575
+ else
2576
+ raise ArgumentError, "#{name}: Value must be an array with exactly 2 elements [min_value, max_value], or a Range"
2577
+ end
2578
+ end
2579
+
2580
+ private
2581
+
2582
+ # @return [Hash] the compiled constraint for a Ruby Range value.
2583
+ def build_range(range)
2584
+ bounds = {}
2585
+
2586
+ unless range.begin.nil?
2587
+ bounds[Parse::Constraint::GreaterThanOrEqualConstraint.key] = Parse::Constraint.formatted_value(range.begin)
2588
+ end
2589
+
2590
+ unless range.end.nil?
2591
+ upper_key = range.exclude_end? ? Parse::Constraint::LessThanConstraint.key : Parse::Constraint::LessThanOrEqualConstraint.key
2592
+ bounds[upper_key] = Parse::Constraint.formatted_value(range.end)
2593
+ end
2594
+
2595
+ if bounds.empty?
2596
+ raise ArgumentError, "#{self.class}: Range must have a begin, an end, or both (ex. 5.., ..25, 5..25)"
2597
+ end
2598
+
2599
+ { @operation.operand => bounds }
2600
+ end
2545
2601
  end
2546
2602
 
2547
2603
  # @!visibility private
data/lib/parse/query.rb CHANGED
@@ -1113,6 +1113,77 @@ module Parse
1113
1113
  copy_query
1114
1114
  end
1115
1115
 
1116
+ # Add a "field is NOT between" condition — the logical negation of
1117
+ # `.where(field.between => value)`: `field < min OR field > max` for a
1118
+ # fully-bounded Range/Array, or a single one-sided comparison when the
1119
+ # Range is beginless/endless (mirroring how {Parse::Constraint::BetweenConstraint}
1120
+ # itself only constrains the side that is present).
1121
+ #
1122
+ # Not available as a `field.not_between => value` symbol constraint,
1123
+ # unlike `.between`: a between-style range is inherently an OR of two
1124
+ # comparisons, and a single {Parse::Constraint}'s `#build` can only
1125
+ # safely contribute an AND'd clause to a query. A constraint that
1126
+ # unilaterally emitted a top-level `$or` would collide with (and
1127
+ # silently clobber, or be clobbered by) any other `$or` this query
1128
+ # already produces via {#or_where} / `|` / another `where_not_between`
1129
+ # call, since only one `$or` group merges correctly per query. This
1130
+ # method instead composes the negation the same way {Parse::Query.and}
1131
+ # does — concatenating compiled constraint arrays — which correctly
1132
+ # nests the OR inside the query's existing AND'd conditions instead of
1133
+ # replacing them the way {#or_where} would.
1134
+ #
1135
+ # @example
1136
+ # Person.query.where_not_between(:age, 5..25)
1137
+ # # age < 5 OR age > 25
1138
+ #
1139
+ # Record.query.where(:archived => false).where_not_between(:date, 5.days.ago...2.days.ago)
1140
+ # # archived == false AND (date < 5.days.ago OR date >= 2.days.ago)
1141
+ #
1142
+ # @param field [Symbol, String] the field to constrain.
1143
+ # @param value [Range, Array] a `between`-style value: a Range (including
1144
+ # beginless/endless/exclusive-end forms) or a 2-element `[min, max]` Array.
1145
+ # @return [self]
1146
+ # @raise [ArgumentError] if `value` isn't a Range or 2-element Array, or
1147
+ # is a fully-open (`nil..nil`) Range.
1148
+ def where_not_between(field, value)
1149
+ field = field.to_sym
1150
+ min_value, max_value, exclude_max = Parse::Constraint::BetweenConstraint.extract_bounds(value)
1151
+
1152
+ if min_value.nil? && max_value.nil?
1153
+ raise ArgumentError, "Query#where_not_between: Range must have a begin, an end, or both (ex. 5.., ..25, 5..25)."
1154
+ end
1155
+
1156
+ # A fully-bounded range needs its own `$or` group (`field < min OR
1157
+ # field > max`). Only ONE `$or` group survives the plain-Hash merge
1158
+ # every constraint's compiled output goes through (`constraint_reduce`
1159
+ # deep-merges compiled hashes; a second top-level `$or` key silently
1160
+ # overwrites the first rather than combining with it — the same
1161
+ # constraint that makes `field.not_between => value` unsafe as a
1162
+ # symbol constraint, see above). Fail loudly here instead of quietly
1163
+ # dropping half the query if this query already has one, from
1164
+ # `#or_where`, `|`, or an earlier `where_not_between` call.
1165
+ if min_value && max_value && @where.any? { |c| c.is_a?(Parse::Constraint::CompoundQueryConstraint) }
1166
+ raise ArgumentError,
1167
+ "Query#where_not_between: this query already has an `$or` group (from `or_where`, `|`, " \
1168
+ "or a prior `where_not_between` call). Only one `$or` group can be safely merged per " \
1169
+ "query. Compose the two queries with Parse::Query.and(...) instead."
1170
+ end
1171
+
1172
+ negated = if min_value.nil?
1173
+ Parse::Query.new(@table).where(field.public_send(exclude_max ? :gte : :gt) => max_value)
1174
+ elsif max_value.nil?
1175
+ Parse::Query.new(@table).where(field.lt => min_value)
1176
+ else
1177
+ lower = Parse::Query.new(@table).where(field.lt => min_value)
1178
+ upper = Parse::Query.new(@table).where(field.public_send(exclude_max ? :gte : :gt) => max_value)
1179
+ Parse::Query.or(lower, upper)
1180
+ end
1181
+
1182
+ @where = @where + negated.where
1183
+ @results = nil
1184
+ self
1185
+ end
1186
+
1116
1187
  # Queries can be made using distinct, allowing you find unique values for a specified field.
1117
1188
  # For this to be performant, please remember to index your database.
1118
1189
  # @example
@@ -1409,7 +1480,6 @@ module Parse
1409
1480
  return first_direct(limit_or_constraints)
1410
1481
  end
1411
1482
 
1412
- fetch_count = 1
1413
1483
  if limit_or_constraints.is_a?(Hash)
1414
1484
  conditions(limit_or_constraints)
1415
1485
  # Check if limit was set in constraints, otherwise use 1
@@ -1490,15 +1560,19 @@ module Parse
1490
1560
  # @return [Parse::Object] the object with the given ID.
1491
1561
  # @raise [Parse::Error] if the object is not found.
1492
1562
  def get(object_id)
1493
- parse_class = Object.const_get(@table) if Object.const_defined?(@table)
1494
- parse_class ||= Parse::Object
1495
-
1496
1563
  response = client.fetch_object(@table, object_id)
1497
1564
  if response.error?
1498
1565
  raise Parse::Error.new(response.code, response.error)
1499
1566
  end
1500
1567
 
1501
- Parse::Object.build(response.result, parse_class)
1568
+ # Pass the table name through as-is rather than pre-resolving it to
1569
+ # a Class: `Object.build` does its own `Parse::Model.find_class`
1570
+ # lookup against the String, which correctly honors `parse_class`
1571
+ # aliasing. Resolving to a Class first and handing that back to
1572
+ # `build` broke aliased lookups, since `find_class` would then
1573
+ # stringify the Ruby constant name (e.g. "Musician") instead of
1574
+ # matching the declared alias (e.g. "Artist").
1575
+ Parse::Object.build(response.result, @table)
1502
1576
  end
1503
1577
 
1504
1578
  # max_results is used to iterate through as many API requests as possible using
@@ -6,6 +6,6 @@ module Parse
6
6
  # The Parse Server SDK for Ruby
7
7
  module Stack
8
8
  # The current version.
9
- VERSION = "5.7.0"
9
+ VERSION = "5.7.2"
10
10
  end
11
11
  end
@@ -132,6 +132,46 @@ module Parse
132
132
 
133
133
  class << self
134
134
 
135
+ # Whether an exception raised by one `after_*` handler prevents the
136
+ # remaining handlers for that same trigger from running.
137
+ #
138
+ # Only the accumulating, non-rejectable `after_*` triggers can have more
139
+ # than one handler (see {Parse::Webhooks::Registration#route}), so this
140
+ # governs `after_save`, `after_delete`, and `after_logout` and nothing
141
+ # else. `before_*` dispatch is untouched: a raise there is how a handler
142
+ # denies an operation, and it must continue to abort.
143
+ #
144
+ # Defaults to `true`, which is the historical behavior. Handlers are
145
+ # folded with `Array#map`, and `map` abandons the collection on the first
146
+ # raise, so a handler that raises silently prevents every handler
147
+ # registered after it from running. That ordering is not something an
148
+ # application fully controls: the SDK's own cache-invalidation triggers
149
+ # install during `Parse.setup` and therefore sit ahead of handlers
150
+ # registered by application files loaded later.
151
+ #
152
+ # Set to `false` to isolate handlers from each other, so that each one
153
+ # runs regardless of what an earlier one raised. The error is reported
154
+ # (a warning plus a `parse.webhooks.handler_error` notification) and
155
+ # dispatch continues.
156
+ #
157
+ # Either way, nothing is reverted. An `after_*` trigger fires once the
158
+ # write has already committed, so there is no version of this setting
159
+ # that can undo the save; the only question it answers is whether the
160
+ # remaining handlers still get to run.
161
+ #
162
+ # @example Keep one failing handler from starving the others
163
+ # Parse::Webhooks.abort_after_callbacks_on_error = false
164
+ #
165
+ # @return [Boolean]
166
+ attr_writer :abort_after_callbacks_on_error
167
+
168
+ # (see #abort_after_callbacks_on_error=)
169
+ # @return [Boolean]
170
+ def abort_after_callbacks_on_error
171
+ return @abort_after_callbacks_on_error unless @abort_after_callbacks_on_error.nil?
172
+ true
173
+ end
174
+
135
175
  # Allows support for web frameworks that support auto-reloading of source.
136
176
  # @!visibility private
137
177
  def reload!(args = {})
@@ -245,6 +285,60 @@ module Parse
245
285
  # block that declares a parameter (`do |payload| ... end`) or a splat
246
286
  # receives the payload.
247
287
  #
288
+ # Run every handler registered for one accumulating `after_*` trigger.
289
+ #
290
+ # `.last` is preserved as the composed result because Parse Server
291
+ # ignores the response body for these triggers and {#call_route}
292
+ # normalizes it anyway, so which handler's value survives is not
293
+ # observable.
294
+ #
295
+ # When {abort_after_callbacks_on_error} is false, a handler that raises
296
+ # is reported and skipped rather than taking the rest of the trigger down
297
+ # with it. Nothing is reverted in either mode: the write these triggers
298
+ # fire on has already committed.
299
+ #
300
+ # @param payload [Parse::Webhooks::Payload] the request payload.
301
+ # @param registry [Array<Proc>] the handlers, in registration order.
302
+ # @param type [Symbol] the trigger being dispatched.
303
+ # @return [Object] the last handler result.
304
+ def dispatch_composed(payload, registry, type)
305
+ return registry.map { |hook| invoke_handler(payload, hook) }.last if
306
+ abort_after_callbacks_on_error
307
+
308
+ last = nil
309
+ registry.each do |hook|
310
+ begin
311
+ last = invoke_handler(payload, hook)
312
+ rescue StandardError => e
313
+ report_handler_error(type, e)
314
+ end
315
+ end
316
+ last
317
+ end
318
+
319
+ # Report a handler failure that was isolated rather than propagated.
320
+ #
321
+ # The message is included because an application's own handler raised it
322
+ # and the application needs it to debug; this is not the SDK's internal
323
+ # `guard`, which deliberately omits messages that can carry a cache key.
324
+ #
325
+ # @param type [Symbol] the trigger being dispatched.
326
+ # @param error [StandardError] the raised error.
327
+ # @return [void]
328
+ def report_handler_error(type, error)
329
+ warn "[Parse::Webhooks] #{type} handler raised #{error.class}: #{error.message}; " \
330
+ "continuing with the remaining handlers " \
331
+ "(Parse::Webhooks.abort_after_callbacks_on_error is false)"
332
+ return unless defined?(ActiveSupport::Notifications)
333
+ begin
334
+ ActiveSupport::Notifications.instrument(
335
+ "parse.webhooks.handler_error", trigger: type, error: error.class.name,
336
+ )
337
+ rescue StandardError
338
+ nil
339
+ end
340
+ end
341
+
248
342
  # @param payload [Parse::Webhooks::Payload] the request payload (becomes `self`).
249
343
  # @param block [Proc] the registered handler block.
250
344
  # @return [Object] the handler's result value.
@@ -350,7 +444,6 @@ module Parse
350
444
  payload.instance_variable_set(:@ruby_initiated, ruby_initiated)
351
445
  trusted_ruby_initiated = ruby_initiated && (payload.master? == true)
352
446
  else
353
- ruby_initiated = false
354
447
  trusted_ruby_initiated = false
355
448
  end
356
449
 
@@ -378,7 +471,10 @@ module Parse
378
471
  end
379
472
 
380
473
  if registry.is_a?(Array)
381
- result = registry.map { |hook| invoke_handler(payload, hook) }.last
474
+ # An Array registry only ever exists for the accumulating,
475
+ # non-rejectable `after_*` triggers, so isolating handlers here
476
+ # cannot affect `before_*` rejection semantics.
477
+ result = dispatch_composed(payload, registry, type)
382
478
  else
383
479
  result = invoke_handler(payload, registry)
384
480
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: parse-stack-next
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.7.0
4
+ version: 5.7.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adrian Curtin