parse-stack-next 5.7.1 → 5.7.3
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 +163 -0
- data/bin/parse-console +108 -16
- data/examples/rag_chatbot.rb +5 -2
- data/lib/parse/agent/mcp_client.rb +46 -12
- data/lib/parse/agent/mcp_dispatcher.rb +2 -2
- data/lib/parse/agent.rb +0 -1
- data/lib/parse/client/body_builder.rb +12 -5
- data/lib/parse/client/logging.rb +23 -8
- data/lib/parse/client/request.rb +16 -4
- data/lib/parse/client.rb +34 -4
- data/lib/parse/console.rb +9 -3
- data/lib/parse/embeddings/image_fetch.rb +9 -1
- data/lib/parse/embeddings.rb +17 -4
- data/lib/parse/model/core/embed_managed.rb +41 -4
- data/lib/parse/model/core/querying.rb +0 -4
- data/lib/parse/model/object.rb +0 -1
- data/lib/parse/query/constraints.rb +61 -5
- data/lib/parse/query.rb +83 -8
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +1 -0
- data/lib/parse/terminal_safe.rb +138 -0
- data/lib/parse/webhooks.rb +29 -13
- metadata +2 -1
data/lib/parse/client.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "faraday"
|
|
4
|
+
require_relative "terminal_safe"
|
|
4
5
|
|
|
5
6
|
# Attempt to load the persistent connection adapter for better performance.
|
|
6
7
|
# Falls back gracefully to the default adapter if not available.
|
|
@@ -505,23 +506,52 @@ module Parse
|
|
|
505
506
|
end
|
|
506
507
|
|
|
507
508
|
# @!visibility private
|
|
508
|
-
# Emit a redacted warning about a Parse::Response error
|
|
509
|
+
# Emit a redacted warning about a Parse::Response error.
|
|
509
510
|
#
|
|
510
511
|
# Routes the response error string through
|
|
511
512
|
# {Parse::Middleware::BodyBuilder.redact} to strip credentials (passwords,
|
|
512
513
|
# tokens, sessionTokens, access_tokens, authData) before logging, and
|
|
513
514
|
# truncates to {SAFE_WARN_MAX_ERROR_LENGTH} chars.
|
|
514
515
|
#
|
|
516
|
+
# Writes through {Parse::Middleware::Logging.logger} when the app has
|
|
517
|
+
# configured one (`Parse.logger = ...`), so these warnings land wherever
|
|
518
|
+
# the rest of the app's Parse request/response logging goes instead of
|
|
519
|
+
# bypassing it. Falls back to plain `warn` (STDERR) when no logger is
|
|
520
|
+
# configured, matching prior behavior. Every call site immediately
|
|
521
|
+
# raises the corresponding typed {Parse::Error} right after calling
|
|
522
|
+
# this method, so a misbehaving app-supplied logger (closed handle,
|
|
523
|
+
# full disk, a remote-aggregator client that raises on socket error)
|
|
524
|
+
# must not be allowed to propagate in its place and mask the real
|
|
525
|
+
# error — falls back to `warn` if the logger itself raises.
|
|
526
|
+
#
|
|
515
527
|
# @param tag [String] the bracketed prefix (e.g. "AuthenticationError").
|
|
516
528
|
# @param response [Parse::Response] the response carrying the error.
|
|
517
529
|
# @param name [String, nil] optional cloud-function or job name for context.
|
|
518
530
|
# @return [nil]
|
|
519
531
|
def _safe_warn(tag, response, name: nil)
|
|
532
|
+
# The server's error text and the request description both carry stored
|
|
533
|
+
# values through verbatim, and this lands in a log file or on a
|
|
534
|
+
# terminal. Escape control characters and newlines so a stored value
|
|
535
|
+
# can neither drive the terminal nor forge a second log record.
|
|
520
536
|
err = Parse::Middleware::BodyBuilder.redact(response.error.to_s)[0, SAFE_WARN_MAX_ERROR_LENGTH]
|
|
521
|
-
|
|
522
|
-
|
|
537
|
+
err = Parse::TerminalSafe.sanitize_line(err)
|
|
538
|
+
msg = if name
|
|
539
|
+
"[Parse:#{tag}] `#{Parse::TerminalSafe.sanitize_line(name)}` " \
|
|
540
|
+
"[#{response.code}] #{err} (HTTP #{response.http_status})"
|
|
541
|
+
else
|
|
542
|
+
"[Parse:#{tag}] [E-#{response.code}] " \
|
|
543
|
+
"#{Parse::TerminalSafe.sanitize_line(response.request)} : #{err} " \
|
|
544
|
+
"(#{response.http_status})"
|
|
545
|
+
end
|
|
546
|
+
logger = Parse::Middleware::Logging.logger
|
|
547
|
+
if logger
|
|
548
|
+
begin
|
|
549
|
+
logger.warn(msg)
|
|
550
|
+
rescue StandardError
|
|
551
|
+
warn msg
|
|
552
|
+
end
|
|
523
553
|
else
|
|
524
|
-
warn
|
|
554
|
+
warn msg
|
|
525
555
|
end
|
|
526
556
|
nil
|
|
527
557
|
end
|
data/lib/parse/console.rb
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
# tests / fixtures.
|
|
24
24
|
|
|
25
25
|
require "timeout"
|
|
26
|
+
require_relative "terminal_safe"
|
|
26
27
|
|
|
27
28
|
module Parse
|
|
28
29
|
module Console
|
|
@@ -65,7 +66,10 @@ module Parse
|
|
|
65
66
|
events = Array(on || DEFAULT_WATCH_EVENTS).map(&:to_sym)
|
|
66
67
|
printer = block_given? ? block : ->(ev, obj) {
|
|
67
68
|
title = obj.respond_to?(:id) ? obj.id : obj.inspect
|
|
68
|
-
|
|
69
|
+
# The row is tenant data arriving over a live-query socket and this
|
|
70
|
+
# line goes straight to the operator's terminal, so escape it.
|
|
71
|
+
puts "[#{Time.now.iso8601}] #{klass.parse_class}.#{ev} " \
|
|
72
|
+
"#{Parse::TerminalSafe.sanitize_line(title)}"
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
delivered = 0
|
|
@@ -78,11 +82,13 @@ module Parse
|
|
|
78
82
|
begin
|
|
79
83
|
printer.call(ev, obj)
|
|
80
84
|
rescue StandardError => e
|
|
81
|
-
|
|
85
|
+
# The message can quote the row that triggered it.
|
|
86
|
+
warn "[Parse.watch] handler raised #{e.class}: " \
|
|
87
|
+
"#{Parse::TerminalSafe.sanitize_line(e.message)}"
|
|
82
88
|
end
|
|
83
89
|
end
|
|
84
90
|
end
|
|
85
|
-
sub.on(:error) { |err| warn "[Parse.watch] error: #{err}" }
|
|
91
|
+
sub.on(:error) { |err| warn "[Parse.watch] error: #{Parse::TerminalSafe.sanitize_line(err)}" }
|
|
86
92
|
|
|
87
93
|
_block_until_interrupt
|
|
88
94
|
delivered
|
|
@@ -164,7 +164,15 @@ module Parse
|
|
|
164
164
|
|
|
165
165
|
mime = verify!(bytes, url: canonical)
|
|
166
166
|
bytes = strip_metadata(bytes, mime) if exif_strip
|
|
167
|
-
|
|
167
|
+
# Store the query-stripped URL, not `canonical` verbatim: when
|
|
168
|
+
# `url` is a presigned URL (a private-bucket file adapter),
|
|
169
|
+
# `canonical` carries a live signature, and FetchedImage#url is
|
|
170
|
+
# purely informational from here on (nothing re-fetches it).
|
|
171
|
+
# Keeping the signature out of the struct preserves the
|
|
172
|
+
# log-safety `#inspect` below was written for — third-party
|
|
173
|
+
# provider adapters and error reporters that capture locals
|
|
174
|
+
# would otherwise leak a valid bearer credential through it.
|
|
175
|
+
FetchedImage.new(bytes: bytes, mime_type: mime, url: Parse::File.strip_query(canonical))
|
|
168
176
|
end
|
|
169
177
|
|
|
170
178
|
# Verify raw bytes: sniff the magic, check the allowlist, and
|
data/lib/parse/embeddings.rb
CHANGED
|
@@ -382,6 +382,17 @@ module Parse
|
|
|
382
382
|
# `"true"`, or a non-matching String) raises
|
|
383
383
|
# {ConfirmationRequired}. Reset to `nil` to disable.
|
|
384
384
|
#
|
|
385
|
+
# For a `:file` source backed by a private-bucket adapter
|
|
386
|
+
# (S3/GCS with server-side presigning), the URL forwarded under
|
|
387
|
+
# this sentinel may be the file's presigned URL rather than its
|
|
388
|
+
# bare canonical one — a time-limited bearer credential for that
|
|
389
|
+
# object, not just a pointer to it (see {Parse::File#presigned_url}
|
|
390
|
+
# and `Parse::Core::EmbedManaged.embed_image`). Reviewing the
|
|
391
|
+
# provider's egress behavior before setting this sentinel should
|
|
392
|
+
# account for that: the provider (and anyone with access to its
|
|
393
|
+
# request logs) gains temporary read access to the object for
|
|
394
|
+
# however long the signature remains valid.
|
|
395
|
+
#
|
|
385
396
|
# @param value [String, nil] {TRUST_PROVIDER_URL_FETCH_SENTINEL} or nil.
|
|
386
397
|
# @raise [ConfirmationRequired] on any other value.
|
|
387
398
|
def trust_provider_url_fetch=(value)
|
|
@@ -395,10 +406,12 @@ module Parse
|
|
|
395
406
|
"String #{TRUST_PROVIDER_URL_FETCH_SENTINEL.inspect}. Plain `true` and " \
|
|
396
407
|
"other values are refused — forwarding image URLs to a third-party " \
|
|
397
408
|
"provider lets that provider issue an HTTP request from its own network " \
|
|
398
|
-
"with attacker-controllable host/path
|
|
399
|
-
"
|
|
400
|
-
"
|
|
401
|
-
"
|
|
409
|
+
"with attacker-controllable host/path, and for a private-bucket file may " \
|
|
410
|
+
"hand it a time-limited presigned URL rather than a bare pointer. Set the " \
|
|
411
|
+
"sentinel only after you have configured " \
|
|
412
|
+
"Parse::Embeddings.allowed_image_hosts AND reviewed the provider's " \
|
|
413
|
+
"documented egress behavior (DNS rebinding window, redirect policy, " \
|
|
414
|
+
"request-log retention)."
|
|
402
415
|
end
|
|
403
416
|
CONFIG_MUTEX.synchronize { @trust_provider_url_fetch = value }
|
|
404
417
|
end
|
|
@@ -687,7 +687,7 @@ module Parse
|
|
|
687
687
|
return if stored_digest == digest && target_present
|
|
688
688
|
|
|
689
689
|
provider = Parse::Embeddings.provider(directive.provider_name)
|
|
690
|
-
vectors = call_provider(provider, directive, input)
|
|
690
|
+
vectors = call_provider(provider, directive, input, record)
|
|
691
691
|
unless vectors.is_a?(Array) && vectors.length == 1 && vectors.first.is_a?(Array)
|
|
692
692
|
raise Parse::Embeddings::InvalidResponseError,
|
|
693
693
|
"Parse::Core::EmbedManaged (#{record.class}##{directive.into}): provider " \
|
|
@@ -774,16 +774,27 @@ module Parse
|
|
|
774
774
|
# provider a {Parse::Embeddings::ImageFetch::FetchedImage}; `:url`
|
|
775
775
|
# mode forwards the raw URL String (the provider validates and
|
|
776
776
|
# fetches it itself).
|
|
777
|
-
|
|
777
|
+
#
|
|
778
|
+
# `input` is the bare canonical URL used for the digest (see
|
|
779
|
+
# {.build_source_input}). It stays stable across saves, so an
|
|
780
|
+
# unsigned re-read of the same file location does not force a
|
|
781
|
+
# re-embed.
|
|
782
|
+
# The actual fetch/forward target prefers the file's presigned
|
|
783
|
+
# URL ({Parse::File#presigned_url}) when one is currently valid,
|
|
784
|
+
# since a private-bucket adapter's bare `file.url` is stripped of
|
|
785
|
+
# its signature and will not resolve for the provider or for the
|
|
786
|
+
# SDK's own `:bytes`-mode download.
|
|
787
|
+
def self.call_provider(provider, directive, input, record)
|
|
778
788
|
if directive.image?
|
|
789
|
+
fetch_url = presigned_fetch_url(record, directive, input)
|
|
779
790
|
source = if directive.bytes_mode?
|
|
780
791
|
Parse::Embeddings::ImageFetch.fetch!(
|
|
781
|
-
|
|
792
|
+
fetch_url,
|
|
782
793
|
allow_insecure: directive.allow_insecure ? true : false,
|
|
783
794
|
exif_strip: directive.exif_strip != false,
|
|
784
795
|
)
|
|
785
796
|
else
|
|
786
|
-
|
|
797
|
+
fetch_url
|
|
787
798
|
end
|
|
788
799
|
provider.embed_image([source],
|
|
789
800
|
input_type: directive.input_type,
|
|
@@ -793,6 +804,32 @@ module Parse
|
|
|
793
804
|
end
|
|
794
805
|
end
|
|
795
806
|
|
|
807
|
+
# @!visibility private
|
|
808
|
+
# Resolve the URL to actually fetch/forward for an image
|
|
809
|
+
# directive: the source file's currently-valid presigned URL if
|
|
810
|
+
# it has one, otherwise the bare canonical `fallback` (the same
|
|
811
|
+
# string used for the digest). Never used for text directives, so
|
|
812
|
+
# `directive.sources.first` is always a `:file` property here.
|
|
813
|
+
#
|
|
814
|
+
# Checks validity with a zero safety buffer rather than
|
|
815
|
+
# {Parse::File#presigned_url_valid?}'s default 60-second one. That
|
|
816
|
+
# default exists so a browser has time to render before a
|
|
817
|
+
# presigned URL goes stale; here it would instead spend the last
|
|
818
|
+
# 60 seconds of a perfectly usable presigned URL falling back to
|
|
819
|
+
# `fallback`, which on a private-bucket adapter is not fetchable
|
|
820
|
+
# at all. A fetch that starts immediately after this check has no
|
|
821
|
+
# meaningful use for that margin, and a 403 from a URL that
|
|
822
|
+
# expired mid-request is strictly better than a guaranteed 403
|
|
823
|
+
# from a URL known unfetchable in advance.
|
|
824
|
+
def self.presigned_fetch_url(record, directive, fallback)
|
|
825
|
+
file = record.public_send(directive.sources.first)
|
|
826
|
+
if file.respond_to?(:presigned_url_valid?) && file.presigned_url_valid?(buffer: 0)
|
|
827
|
+
file.presigned_url
|
|
828
|
+
else
|
|
829
|
+
fallback
|
|
830
|
+
end
|
|
831
|
+
end
|
|
832
|
+
|
|
796
833
|
# @!visibility private
|
|
797
834
|
# Concatenate source-field string values. `nil` and blank entries
|
|
798
835
|
# are skipped; remaining values are joined with a double newline.
|
|
@@ -253,7 +253,6 @@ module Parse
|
|
|
253
253
|
# same created_at date (down to the microsecond). This prevents getting the same
|
|
254
254
|
# record in the next query request.
|
|
255
255
|
exclusion_set = results.select { |r| r.created_at == next_cursor.created_at }.map(&:id)
|
|
256
|
-
results = nil
|
|
257
256
|
cursor = next_cursor
|
|
258
257
|
end
|
|
259
258
|
end
|
|
@@ -356,7 +355,6 @@ module Parse
|
|
|
356
355
|
# Object.latest(:user.eq => user, limit: 5) # => 5 most recent for user
|
|
357
356
|
# @return [Parse::Object] the most recently created object matching constraints.
|
|
358
357
|
def latest(constraints = {})
|
|
359
|
-
fetch_count = 1
|
|
360
358
|
if constraints.is_a?(Numeric)
|
|
361
359
|
fetch_count = constraints.to_i
|
|
362
360
|
constraints = {}
|
|
@@ -385,7 +383,6 @@ module Parse
|
|
|
385
383
|
# Object.last_updated(:user.eq => user, limit: 3) # => 3 most recently updated for user
|
|
386
384
|
# @return [Parse::Object] the most recently updated object matching constraints.
|
|
387
385
|
def last_updated(constraints = {})
|
|
388
|
-
fetch_count = 1
|
|
389
386
|
if constraints.is_a?(Numeric)
|
|
390
387
|
fetch_count = constraints.to_i
|
|
391
388
|
constraints = {}
|
|
@@ -600,7 +597,6 @@ module Parse
|
|
|
600
597
|
parse_ids.compact!
|
|
601
598
|
# determines if the result back to the call site is an array or a single result
|
|
602
599
|
as_array = parse_ids.count > 1
|
|
603
|
-
results = []
|
|
604
600
|
|
|
605
601
|
# Default to write-only cache mode - find always gets fresh data
|
|
606
602
|
# but updates cache for future cached reads. Controlled by feature flag.
|
data/lib/parse/model/object.rb
CHANGED
|
@@ -1867,7 +1867,6 @@ module Parse
|
|
|
1867
1867
|
# we should do a reverse lookup on who is registered for a different class type
|
|
1868
1868
|
# than their name with parse_class
|
|
1869
1869
|
klass = Parse::Model.find_class className
|
|
1870
|
-
o = nil
|
|
1871
1870
|
if klass.present?
|
|
1872
1871
|
# when creating objects from Parse JSON data, don't use dirty tracking since
|
|
1873
1872
|
# we are considering these objects as "pristine"
|
|
@@ -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
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require_relative "client"
|
|
5
|
+
require_relative "terminal_safe"
|
|
5
6
|
require_relative "pipeline_security"
|
|
6
7
|
require_relative "query/operation"
|
|
7
8
|
require_relative "query/constraints"
|
|
@@ -1113,6 +1114,77 @@ module Parse
|
|
|
1113
1114
|
copy_query
|
|
1114
1115
|
end
|
|
1115
1116
|
|
|
1117
|
+
# Add a "field is NOT between" condition — the logical negation of
|
|
1118
|
+
# `.where(field.between => value)`: `field < min OR field > max` for a
|
|
1119
|
+
# fully-bounded Range/Array, or a single one-sided comparison when the
|
|
1120
|
+
# Range is beginless/endless (mirroring how {Parse::Constraint::BetweenConstraint}
|
|
1121
|
+
# itself only constrains the side that is present).
|
|
1122
|
+
#
|
|
1123
|
+
# Not available as a `field.not_between => value` symbol constraint,
|
|
1124
|
+
# unlike `.between`: a between-style range is inherently an OR of two
|
|
1125
|
+
# comparisons, and a single {Parse::Constraint}'s `#build` can only
|
|
1126
|
+
# safely contribute an AND'd clause to a query. A constraint that
|
|
1127
|
+
# unilaterally emitted a top-level `$or` would collide with (and
|
|
1128
|
+
# silently clobber, or be clobbered by) any other `$or` this query
|
|
1129
|
+
# already produces via {#or_where} / `|` / another `where_not_between`
|
|
1130
|
+
# call, since only one `$or` group merges correctly per query. This
|
|
1131
|
+
# method instead composes the negation the same way {Parse::Query.and}
|
|
1132
|
+
# does — concatenating compiled constraint arrays — which correctly
|
|
1133
|
+
# nests the OR inside the query's existing AND'd conditions instead of
|
|
1134
|
+
# replacing them the way {#or_where} would.
|
|
1135
|
+
#
|
|
1136
|
+
# @example
|
|
1137
|
+
# Person.query.where_not_between(:age, 5..25)
|
|
1138
|
+
# # age < 5 OR age > 25
|
|
1139
|
+
#
|
|
1140
|
+
# Record.query.where(:archived => false).where_not_between(:date, 5.days.ago...2.days.ago)
|
|
1141
|
+
# # archived == false AND (date < 5.days.ago OR date >= 2.days.ago)
|
|
1142
|
+
#
|
|
1143
|
+
# @param field [Symbol, String] the field to constrain.
|
|
1144
|
+
# @param value [Range, Array] a `between`-style value: a Range (including
|
|
1145
|
+
# beginless/endless/exclusive-end forms) or a 2-element `[min, max]` Array.
|
|
1146
|
+
# @return [self]
|
|
1147
|
+
# @raise [ArgumentError] if `value` isn't a Range or 2-element Array, or
|
|
1148
|
+
# is a fully-open (`nil..nil`) Range.
|
|
1149
|
+
def where_not_between(field, value)
|
|
1150
|
+
field = field.to_sym
|
|
1151
|
+
min_value, max_value, exclude_max = Parse::Constraint::BetweenConstraint.extract_bounds(value)
|
|
1152
|
+
|
|
1153
|
+
if min_value.nil? && max_value.nil?
|
|
1154
|
+
raise ArgumentError, "Query#where_not_between: Range must have a begin, an end, or both (ex. 5.., ..25, 5..25)."
|
|
1155
|
+
end
|
|
1156
|
+
|
|
1157
|
+
# A fully-bounded range needs its own `$or` group (`field < min OR
|
|
1158
|
+
# field > max`). Only ONE `$or` group survives the plain-Hash merge
|
|
1159
|
+
# every constraint's compiled output goes through (`constraint_reduce`
|
|
1160
|
+
# deep-merges compiled hashes; a second top-level `$or` key silently
|
|
1161
|
+
# overwrites the first rather than combining with it — the same
|
|
1162
|
+
# constraint that makes `field.not_between => value` unsafe as a
|
|
1163
|
+
# symbol constraint, see above). Fail loudly here instead of quietly
|
|
1164
|
+
# dropping half the query if this query already has one, from
|
|
1165
|
+
# `#or_where`, `|`, or an earlier `where_not_between` call.
|
|
1166
|
+
if min_value && max_value && @where.any? { |c| c.is_a?(Parse::Constraint::CompoundQueryConstraint) }
|
|
1167
|
+
raise ArgumentError,
|
|
1168
|
+
"Query#where_not_between: this query already has an `$or` group (from `or_where`, `|`, " \
|
|
1169
|
+
"or a prior `where_not_between` call). Only one `$or` group can be safely merged per " \
|
|
1170
|
+
"query. Compose the two queries with Parse::Query.and(...) instead."
|
|
1171
|
+
end
|
|
1172
|
+
|
|
1173
|
+
negated = if min_value.nil?
|
|
1174
|
+
Parse::Query.new(@table).where(field.public_send(exclude_max ? :gte : :gt) => max_value)
|
|
1175
|
+
elsif max_value.nil?
|
|
1176
|
+
Parse::Query.new(@table).where(field.lt => min_value)
|
|
1177
|
+
else
|
|
1178
|
+
lower = Parse::Query.new(@table).where(field.lt => min_value)
|
|
1179
|
+
upper = Parse::Query.new(@table).where(field.public_send(exclude_max ? :gte : :gt) => max_value)
|
|
1180
|
+
Parse::Query.or(lower, upper)
|
|
1181
|
+
end
|
|
1182
|
+
|
|
1183
|
+
@where = @where + negated.where
|
|
1184
|
+
@results = nil
|
|
1185
|
+
self
|
|
1186
|
+
end
|
|
1187
|
+
|
|
1116
1188
|
# Queries can be made using distinct, allowing you find unique values for a specified field.
|
|
1117
1189
|
# For this to be performant, please remember to index your database.
|
|
1118
1190
|
# @example
|
|
@@ -1409,7 +1481,6 @@ module Parse
|
|
|
1409
1481
|
return first_direct(limit_or_constraints)
|
|
1410
1482
|
end
|
|
1411
1483
|
|
|
1412
|
-
fetch_count = 1
|
|
1413
1484
|
if limit_or_constraints.is_a?(Hash)
|
|
1414
1485
|
conditions(limit_or_constraints)
|
|
1415
1486
|
# Check if limit was set in constraints, otherwise use 1
|
|
@@ -1490,15 +1561,19 @@ module Parse
|
|
|
1490
1561
|
# @return [Parse::Object] the object with the given ID.
|
|
1491
1562
|
# @raise [Parse::Error] if the object is not found.
|
|
1492
1563
|
def get(object_id)
|
|
1493
|
-
parse_class = Object.const_get(@table) if Object.const_defined?(@table)
|
|
1494
|
-
parse_class ||= Parse::Object
|
|
1495
|
-
|
|
1496
1564
|
response = client.fetch_object(@table, object_id)
|
|
1497
1565
|
if response.error?
|
|
1498
1566
|
raise Parse::Error.new(response.code, response.error)
|
|
1499
1567
|
end
|
|
1500
1568
|
|
|
1501
|
-
|
|
1569
|
+
# Pass the table name through as-is rather than pre-resolving it to
|
|
1570
|
+
# a Class: `Object.build` does its own `Parse::Model.find_class`
|
|
1571
|
+
# lookup against the String, which correctly honors `parse_class`
|
|
1572
|
+
# aliasing. Resolving to a Class first and handing that back to
|
|
1573
|
+
# `build` broke aliased lookups, since `find_class` would then
|
|
1574
|
+
# stringify the Ruby constant name (e.g. "Musician") instead of
|
|
1575
|
+
# matching the declared alias (e.g. "Artist").
|
|
1576
|
+
Parse::Object.build(response.result, @table)
|
|
1502
1577
|
end
|
|
1503
1578
|
|
|
1504
1579
|
# max_results is used to iterate through as many API requests as possible using
|
|
@@ -1708,7 +1783,7 @@ module Parse
|
|
|
1708
1783
|
def fetch!(compiled_query)
|
|
1709
1784
|
response = client.find_objects(@table, compiled_query.as_json, headers: _headers, **_opts)
|
|
1710
1785
|
if response.error?
|
|
1711
|
-
puts "[ParseQuery] #{response.error}"
|
|
1786
|
+
puts "[ParseQuery] #{Parse::TerminalSafe.sanitize_line(response.error)}"
|
|
1712
1787
|
end
|
|
1713
1788
|
response
|
|
1714
1789
|
end
|
|
@@ -3522,12 +3597,12 @@ module Parse
|
|
|
3522
3597
|
# non-master explain that worked on 8.x now returns a permission
|
|
3523
3598
|
# error. Surface that as actionable guidance instead of a bare 403.
|
|
3524
3599
|
if response.respond_to?(:permission_denied?) && response.permission_denied?
|
|
3525
|
-
puts "[ParseQuery:Explain] #{response.error} — Parse Server 9.0+ defaults " \
|
|
3600
|
+
puts "[ParseQuery:Explain] #{Parse::TerminalSafe.sanitize_line(response.error)} — Parse Server 9.0+ defaults " \
|
|
3526
3601
|
"`allowPublicExplain` to false; query explain now requires the master key " \
|
|
3527
3602
|
"(use_master_key: true) or `allowPublicExplain: true` in the server's " \
|
|
3528
3603
|
"databaseOptions."
|
|
3529
3604
|
else
|
|
3530
|
-
puts "[ParseQuery:Explain] #{response.error}"
|
|
3605
|
+
puts "[ParseQuery:Explain] #{Parse::TerminalSafe.sanitize_line(response.error)}"
|
|
3531
3606
|
end
|
|
3532
3607
|
return {}
|
|
3533
3608
|
end
|
data/lib/parse/stack/version.rb
CHANGED