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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +167 -1
- data/lib/parse/agent/mcp_dispatcher.rb +2 -2
- data/lib/parse/agent.rb +0 -1
- data/lib/parse/cache/invalidation.rb +102 -50
- data/lib/parse/client/request.rb +16 -4
- data/lib/parse/client.rb +25 -4
- data/lib/parse/embeddings/image_fetch.rb +9 -1
- data/lib/parse/embeddings.rb +17 -4
- data/lib/parse/model/associations/belongs_to.rb +4 -0
- data/lib/parse/model/associations/collection_proxy.rb +3 -0
- data/lib/parse/model/associations/has_many.rb +4 -0
- data/lib/parse/model/core/actions.rb +294 -123
- data/lib/parse/model/core/embed_managed.rb +41 -4
- data/lib/parse/model/core/fetching.rb +4 -0
- data/lib/parse/model/core/properties.rb +4 -0
- data/lib/parse/model/core/querying.rb +0 -4
- data/lib/parse/model/object.rb +4 -1
- data/lib/parse/query/constraints.rb +61 -5
- data/lib/parse/query.rb +79 -5
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/webhooks.rb +98 -2
- metadata +1 -1
|
@@ -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
|
-
|
|
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
|
data/lib/parse/stack/version.rb
CHANGED
data/lib/parse/webhooks.rb
CHANGED
|
@@ -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
|
-
|
|
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
|