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.
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 to stderr.
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
- if name
522
- warn "[Parse:#{tag}] `#{name}` [#{response.code}] #{err} (HTTP #{response.http_status})"
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 "[Parse:#{tag}] [E-#{response.code}] #{response.request} : #{err} (#{response.http_status})"
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
- puts "[#{Time.now.iso8601}] #{klass.parse_class}.#{ev} #{title}"
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
- warn "[Parse.watch] handler raised #{e.class}: #{e.message}"
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
- FetchedImage.new(bytes: bytes, mime_type: mime, url: canonical)
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
@@ -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. Set the sentinel only after you " \
399
- "have configured Parse::Embeddings.allowed_image_hosts AND reviewed the " \
400
- "provider's documented egress behavior (DNS rebinding window, redirect " \
401
- "policy)."
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
- def self.call_provider(provider, directive, input)
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
- input,
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
- input
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.
@@ -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
- Parse::Object.build(response.result, parse_class)
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
@@ -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.1"
9
+ VERSION = "5.7.3"
10
10
  end
11
11
  end
data/lib/parse/stack.rb CHANGED
@@ -2,6 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require_relative "stack/version"
5
+ require_relative "terminal_safe"
5
6
  require_relative "client"
6
7
  require_relative "query"
7
8
  require_relative "model/object"