parse-stack-next 5.5.6 → 5.7.0

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.
Files changed (119) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +634 -0
  3. data/README.md +378 -6
  4. data/docs/caching.md +748 -0
  5. data/examples/basic_client.rb +3 -3
  6. data/examples/basic_server.rb +3 -3
  7. data/examples/live_query_listener.rb +12 -12
  8. data/examples/rag_chatbot.rb +14 -14
  9. data/examples/transaction_example.rb +44 -45
  10. data/examples/webhook_server.rb +3 -3
  11. data/lib/parse/access.rb +493 -0
  12. data/lib/parse/acl_scope.rb +125 -52
  13. data/lib/parse/agent/approval_gate.rb +0 -0
  14. data/lib/parse/agent/cancellation_token.rb +3 -3
  15. data/lib/parse/agent/constraint_translator.rb +4 -4
  16. data/lib/parse/agent/describe.rb +34 -34
  17. data/lib/parse/agent/errors.rb +9 -9
  18. data/lib/parse/agent/mcp_client.rb +61 -59
  19. data/lib/parse/agent/mcp_dispatcher.rb +89 -101
  20. data/lib/parse/agent/mcp_rack_app.rb +92 -93
  21. data/lib/parse/agent/mcp_server.rb +1 -1
  22. data/lib/parse/agent/mcp_subscriptions.rb +22 -22
  23. data/lib/parse/agent/metadata_audit.rb +1 -2
  24. data/lib/parse/agent/metadata_dsl.rb +17 -7
  25. data/lib/parse/agent/metadata_registry.rb +12 -12
  26. data/lib/parse/agent/prompt_hardening.rb +4 -4
  27. data/lib/parse/agent/prompts.rb +10 -10
  28. data/lib/parse/agent/result_formatter.rb +4 -4
  29. data/lib/parse/agent/tools.rb +493 -400
  30. data/lib/parse/agent.rb +157 -116
  31. data/lib/parse/api/hooks.rb +1 -1
  32. data/lib/parse/api/server.rb +2 -2
  33. data/lib/parse/api/users.rb +2 -2
  34. data/lib/parse/atlas_search/index_manager.rb +1 -1
  35. data/lib/parse/atlas_search/session.rb +40 -218
  36. data/lib/parse/atlas_search.rb +181 -60
  37. data/lib/parse/authorization.rb +466 -0
  38. data/lib/parse/cache/invalidation.rb +167 -0
  39. data/lib/parse/cache/keyspace.rb +306 -0
  40. data/lib/parse/cache/moneta_surface.rb +126 -0
  41. data/lib/parse/cache/pool.rb +48 -5
  42. data/lib/parse/cache/redis.rb +425 -10
  43. data/lib/parse/cache/scoped_view.rb +443 -0
  44. data/lib/parse/cache/sub_cache.rb +264 -0
  45. data/lib/parse/cache/upstream_roles.rb +230 -0
  46. data/lib/parse/client/authentication.rb +1 -1
  47. data/lib/parse/client/body_builder.rb +1 -2
  48. data/lib/parse/client/caching.rb +84 -5
  49. data/lib/parse/client.rb +327 -189
  50. data/lib/parse/clp_scope.rb +225 -28
  51. data/lib/parse/console.rb +3 -3
  52. data/lib/parse/embeddings/batch_embedder.rb +3 -3
  53. data/lib/parse/embeddings/binding_audit.rb +211 -0
  54. data/lib/parse/embeddings/cache.rb +17 -17
  55. data/lib/parse/embeddings/cohere.rb +21 -22
  56. data/lib/parse/embeddings/image_fetch.rb +22 -21
  57. data/lib/parse/embeddings/jina.rb +21 -21
  58. data/lib/parse/embeddings/local_http.rb +6 -7
  59. data/lib/parse/embeddings/media_file.rb +137 -0
  60. data/lib/parse/embeddings/openai.rb +5 -5
  61. data/lib/parse/embeddings/provider.rb +40 -1
  62. data/lib/parse/embeddings/qwen.rb +10 -10
  63. data/lib/parse/embeddings/spend_cap.rb +2 -3
  64. data/lib/parse/embeddings/streaming_body.rb +170 -0
  65. data/lib/parse/embeddings/video_source.rb +121 -0
  66. data/lib/parse/embeddings/voyage.rb +554 -143
  67. data/lib/parse/embeddings.rb +83 -27
  68. data/lib/parse/graphql/type_generator.rb +1 -1
  69. data/lib/parse/graphql.rb +5 -5
  70. data/lib/parse/live_query/client.rb +1 -1
  71. data/lib/parse/live_query.rb +1 -1
  72. data/lib/parse/lock.rb +15 -18
  73. data/lib/parse/lock_backend.rb +1 -1
  74. data/lib/parse/lookup_rewriter.rb +6 -6
  75. data/lib/parse/model/classes/role.rb +227 -28
  76. data/lib/parse/model/classes/user.rb +60 -1
  77. data/lib/parse/model/clp.rb +4 -4
  78. data/lib/parse/model/core/actions.rb +97 -15
  79. data/lib/parse/model/core/create_lock.rb +0 -2
  80. data/lib/parse/model/core/describe.rb +57 -57
  81. data/lib/parse/model/core/embed_managed.rb +43 -19
  82. data/lib/parse/model/core/fetching.rb +2 -0
  83. data/lib/parse/model/core/indexing.rb +14 -14
  84. data/lib/parse/model/core/parse_reference.rb +1 -1
  85. data/lib/parse/model/core/properties.rb +39 -3
  86. data/lib/parse/model/core/querying.rb +1 -1
  87. data/lib/parse/model/core/schema.rb +2 -2
  88. data/lib/parse/model/core/search_indexing.rb +2 -2
  89. data/lib/parse/model/core/vector_searchable.rb +34 -8
  90. data/lib/parse/model/file.rb +21 -15
  91. data/lib/parse/model/geojson.rb +2 -2
  92. data/lib/parse/model/geopoint.rb +1 -0
  93. data/lib/parse/model/object.rb +132 -66
  94. data/lib/parse/model/pointer.rb +2 -0
  95. data/lib/parse/model/polygon.rb +3 -6
  96. data/lib/parse/model/push.rb +2 -2
  97. data/lib/parse/model/vector.rb +3 -1
  98. data/lib/parse/mongodb.rb +512 -282
  99. data/lib/parse/pipeline_security.rb +3 -0
  100. data/lib/parse/query/constraints.rb +46 -46
  101. data/lib/parse/query.rb +146 -59
  102. data/lib/parse/retrieval/agent_tool.rb +34 -20
  103. data/lib/parse/retrieval/chunk.rb +1 -0
  104. data/lib/parse/retrieval/reranker/cohere.rb +11 -11
  105. data/lib/parse/retrieval/reranker.rb +3 -4
  106. data/lib/parse/retrieval/retriever.rb +4 -6
  107. data/lib/parse/schema/index_migrator.rb +26 -26
  108. data/lib/parse/schema/search_index_migrator.rb +19 -19
  109. data/lib/parse/stack/tasks.rb +6 -6
  110. data/lib/parse/stack/version.rb +1 -1
  111. data/lib/parse/stack.rb +10 -10
  112. data/lib/parse/vector_search/hybrid.rb +166 -33
  113. data/lib/parse/vector_search.rb +141 -13
  114. data/lib/parse/webhooks/payload.rb +1 -0
  115. data/lib/parse/webhooks/registration.rb +4 -4
  116. data/lib/parse/webhooks/trigger_audit.rb +50 -49
  117. data/lib/parse/webhooks.rb +22 -4
  118. data/parse-stack-next.gemspec +6 -6
  119. metadata +14 -1
@@ -104,6 +104,7 @@ module Parse
104
104
  class InvalidImageURL < Error
105
105
  # @return [Symbol] failure-mode tag.
106
106
  attr_reader :reason
107
+
107
108
  def initialize(reason, message)
108
109
  @reason = reason
109
110
  super(message)
@@ -132,6 +133,7 @@ module Parse
132
133
  end
133
134
  super(name.to_sym, provider)
134
135
  end
136
+
135
137
  alias_method :store, :[]=
136
138
  end
137
139
 
@@ -247,6 +249,9 @@ module Parse
247
249
  @configuration = nil
248
250
  @allowed_image_hosts = nil
249
251
  @allowed_image_types = nil
252
+ @allowed_video_types = nil
253
+ @max_media_bytes = nil
254
+ BindingAudit.reset!
250
255
  @trust_provider_url_fetch = nil
251
256
  end
252
257
  end
@@ -323,6 +328,54 @@ module Parse
323
328
  @allowed_image_types ||= ImageFetch::DEFAULT_ALLOWED_IMAGE_TYPES
324
329
  end
325
330
 
331
+ # Configure the MIME types {VideoSource.verify!} accepts after
332
+ # magic-byte sniffing. Defaults to
333
+ # {VideoSource::DEFAULT_ALLOWED_VIDEO_TYPES}, which is MP4 only —
334
+ # the sole container Voyage accepts. As with images, the sniffed
335
+ # type — never the `Content-Type` header — is what gets checked.
336
+ #
337
+ # Widening this does NOT widen what a provider accepts: the
338
+ # Voyage adapter still enforces its own format and size limits.
339
+ #
340
+ # @param types [Array<String>] MIME type strings.
341
+ # @return [Array<String>]
342
+ def allowed_video_types=(types)
343
+ unless types.is_a?(Array) && !types.empty? &&
344
+ types.all? { |t| t.is_a?(String) && t.include?("/") }
345
+ raise ArgumentError,
346
+ "Parse::Embeddings.allowed_video_types= expects a non-empty Array of " \
347
+ "MIME type Strings (got #{types.inspect})."
348
+ end
349
+ CONFIG_MUTEX.synchronize { @allowed_video_types = types.dup.freeze }
350
+ end
351
+
352
+ # @return [Array<String>] MIME allowlist for video bytes (frozen).
353
+ def allowed_video_types
354
+ @allowed_video_types ||= VideoSource::DEFAULT_ALLOWED_VIDEO_TYPES
355
+ end
356
+
357
+ # Per-file ceiling for streamed media, checked by
358
+ # {MediaFile}. Voyage documents 20 MB per image and 20 MB per
359
+ # video; the default matches. Streaming keeps an oversized file
360
+ # from exhausting memory, but the provider still rejects it, so
361
+ # failing locally turns a wasted upload into an immediate error.
362
+ #
363
+ # @param bytes [Integer] positive byte count.
364
+ # @return [Integer]
365
+ def max_media_bytes=(bytes)
366
+ unless bytes.is_a?(Integer) && bytes.positive?
367
+ raise ArgumentError,
368
+ "Parse::Embeddings.max_media_bytes= expects a positive Integer " \
369
+ "(got #{bytes.inspect})."
370
+ end
371
+ CONFIG_MUTEX.synchronize { @max_media_bytes = bytes }
372
+ end
373
+
374
+ # @return [Integer] per-file ceiling for {MediaFile}, in bytes.
375
+ def max_media_bytes
376
+ @max_media_bytes ||= MediaFile::DEFAULT_MAX_MEDIA_BYTES
377
+ end
378
+
326
379
  # Sentinel-gated opt-in for forwarding image URLs to embedding
327
380
  # providers. Assign the exact {TRUST_PROVIDER_URL_FETCH_SENTINEL}
328
381
  # String to unlock; any other value (including `true`, `1`,
@@ -396,8 +449,7 @@ module Parse
396
449
  # @raise [InvalidImageURL] on any other validation failure.
397
450
  def validate_image_url!(url, allow_insecure: false, mode: :forward)
398
451
  unless mode == :fetch || trust_provider_url_fetch?
399
- hint =
400
- if allowed_image_hosts.empty?
452
+ hint = if allowed_image_hosts.empty?
401
453
  " First populate Parse::Embeddings.allowed_image_hosts with the CDN " \
402
454
  "hostnames you trust (currently empty — every host would be denied " \
403
455
  "even after the sentinel is set)."
@@ -412,30 +464,30 @@ module Parse
412
464
 
413
465
  unless url.is_a?(String) && !url.empty?
414
466
  raise InvalidImageURL.new(:parse,
415
- "Parse::Embeddings.validate_image_url!: url must be a non-empty String " \
416
- "(got #{url.class}).")
467
+ "Parse::Embeddings.validate_image_url!: url must be a non-empty String " \
468
+ "(got #{url.class}).")
417
469
  end
418
470
 
419
471
  uri = begin
420
- URI.parse(url)
421
- rescue URI::InvalidURIError => e
422
- raise InvalidImageURL.new(:parse,
423
- "Parse::Embeddings.validate_image_url!: invalid URL (#{e.message}).")
424
- end
472
+ URI.parse(url)
473
+ rescue URI::InvalidURIError => e
474
+ raise InvalidImageURL.new(:parse,
475
+ "Parse::Embeddings.validate_image_url!: invalid URL (#{e.message}).")
476
+ end
425
477
 
426
478
  valid_schemes = allow_insecure ? %w[http https] : %w[https]
427
479
  unless valid_schemes.include?(uri.scheme)
428
480
  raise InvalidImageURL.new(:scheme,
429
- "Parse::Embeddings.validate_image_url!: scheme must be #{valid_schemes.join(' or ')} " \
430
- "(got #{uri.scheme.inspect}). Forwarding non-HTTPS image URLs to a provider " \
431
- "leaks any embedded query-string secrets in cleartext.")
481
+ "Parse::Embeddings.validate_image_url!: scheme must be #{valid_schemes.join(" or ")} " \
482
+ "(got #{uri.scheme.inspect}). Forwarding non-HTTPS image URLs to a provider " \
483
+ "leaks any embedded query-string secrets in cleartext.")
432
484
  end
433
485
 
434
486
  if uri.userinfo
435
487
  raise InvalidImageURL.new(:userinfo,
436
- "Parse::Embeddings.validate_image_url!: URL must not include userinfo " \
437
- "credentials. Embedding providers will forward the full URL in their fetch " \
438
- "and may log it.")
488
+ "Parse::Embeddings.validate_image_url!: URL must not include userinfo " \
489
+ "credentials. Embedding providers will forward the full URL in their fetch " \
490
+ "and may log it.")
439
491
  end
440
492
 
441
493
  # `uri.hostname` returns the IDNA-decoded form WITHOUT IPv6
@@ -446,7 +498,7 @@ module Parse
446
498
  host = uri.hostname
447
499
  if host.nil? || host.empty?
448
500
  raise InvalidImageURL.new(:parse,
449
- "Parse::Embeddings.validate_image_url!: URL is missing a host.")
501
+ "Parse::Embeddings.validate_image_url!: URL is missing a host.")
450
502
  end
451
503
 
452
504
  # Reject non-canonical IPv4 forms (decimal `2130706433`,
@@ -463,9 +515,9 @@ module Parse
463
515
  # character).
464
516
  if ip_shaped_but_not_canonical?(host)
465
517
  raise InvalidImageURL.new(:host_blocked,
466
- "Parse::Embeddings.validate_image_url!: host #{host.inspect} is an obfuscated " \
467
- "or non-canonical IP literal. Use dotted-quad IPv4 (a.b.c.d) or canonical IPv6. " \
468
- "Decimal/octal/hex IP forms are refused to prevent localhost-bypass attempts.")
518
+ "Parse::Embeddings.validate_image_url!: host #{host.inspect} is an obfuscated " \
519
+ "or non-canonical IP literal. Use dotted-quad IPv4 (a.b.c.d) or canonical IPv6. " \
520
+ "Decimal/octal/hex IP forms are refused to prevent localhost-bypass attempts.")
469
521
  end
470
522
 
471
523
  # **Image-host allowlist runs BEFORE the resolver hop.** Round-2
@@ -477,9 +529,9 @@ module Parse
477
529
  allowed = allowed_image_hosts
478
530
  if allowed.empty?
479
531
  raise InvalidImageURL.new(:host_not_allowlisted,
480
- "Parse::Embeddings.validate_image_url!: Parse::Embeddings.allowed_image_hosts " \
481
- "is empty — every image URL is denied. Add the CDN hostnames you trust before " \
482
- "forwarding image URLs to a provider.")
532
+ "Parse::Embeddings.validate_image_url!: Parse::Embeddings.allowed_image_hosts " \
533
+ "is empty — every image URL is denied. Add the CDN hostnames you trust before " \
534
+ "forwarding image URLs to a provider.")
483
535
  end
484
536
  permitted = allowed.any? do |entry|
485
537
  if entry.start_with?(".")
@@ -491,8 +543,8 @@ module Parse
491
543
  end
492
544
  unless permitted
493
545
  raise InvalidImageURL.new(:host_not_allowlisted,
494
- "Parse::Embeddings.validate_image_url!: host #{host.inspect} not in " \
495
- "Parse::Embeddings.allowed_image_hosts (#{allowed.inspect}).")
546
+ "Parse::Embeddings.validate_image_url!: host #{host.inspect} not in " \
547
+ "Parse::Embeddings.allowed_image_hosts (#{allowed.inspect}).")
496
548
  end
497
549
 
498
550
  # Port allowlist runs after the host allowlist (cheap string
@@ -502,8 +554,8 @@ module Parse
502
554
  require_relative "model/file"
503
555
  unless Parse::File.allowed_remote_ports.include?(port)
504
556
  raise InvalidImageURL.new(:port,
505
- "Parse::Embeddings.validate_image_url!: port #{port} not in " \
506
- "Parse::File.allowed_remote_ports.")
557
+ "Parse::Embeddings.validate_image_url!: port #{port} not in " \
558
+ "Parse::File.allowed_remote_ports.")
507
559
  end
508
560
 
509
561
  # CIDR + DNS resolution last — most expensive (syscall). An
@@ -516,7 +568,7 @@ module Parse
516
568
  rescue ArgumentError => e
517
569
  tag = e.message.include?("private/internal address") ? :host_blocked : :parse
518
570
  raise InvalidImageURL.new(tag,
519
- "Parse::Embeddings.validate_image_url!: #{e.message}")
571
+ "Parse::Embeddings.validate_image_url!: #{e.message}")
520
572
  end
521
573
 
522
574
  # Return the canonicalized URL so callers store/forward
@@ -590,5 +642,9 @@ require_relative "embeddings/qwen"
590
642
  require_relative "embeddings/local_http"
591
643
  require_relative "embeddings/spend_cap"
592
644
  require_relative "embeddings/image_fetch"
645
+ require_relative "embeddings/video_source"
646
+ require_relative "embeddings/streaming_body"
647
+ require_relative "embeddings/media_file"
648
+ require_relative "embeddings/binding_audit"
593
649
  require_relative "embeddings/cache"
594
650
  require_relative "embeddings/batch_embedder"
@@ -87,7 +87,7 @@ module Parse
87
87
  end
88
88
  collisions = by_gql_name.select { |_, names| names.size > 1 }
89
89
  return if collisions.empty?
90
- details = collisions.map { |gql, parse| "#{gql} ← #{parse.join(', ')}" }.join('; ')
90
+ details = collisions.map { |gql, parse| "#{gql} ← #{parse.join(", ")}" }.join("; ")
91
91
  raise "Parse::GraphQL::TypeGenerator: graphql_name collisions: #{details}. " \
92
92
  "Parse class names that differ only by underscores collapse to the same " \
93
93
  "GraphQL type name. Rename or generate the conflicting classes separately."
data/lib/parse/graphql.rb CHANGED
@@ -26,11 +26,11 @@ module Parse
26
26
  def available?
27
27
  return @gem_available if defined?(@gem_available)
28
28
  @gem_available = begin
29
- require "graphql"
30
- true
31
- rescue LoadError
32
- false
33
- end
29
+ require "graphql"
30
+ true
31
+ rescue LoadError
32
+ false
33
+ end
34
34
  end
35
35
 
36
36
  # Force-reset the cached availability flag. Test-only.
@@ -364,7 +364,7 @@ module Parse
364
364
  # returned subscription and register callbacks later.
365
365
  # @return [Subscription]
366
366
  def subscribe(class_name, where: {}, fields: nil, keys: nil, watch: nil, session_token: nil,
367
- use_master_key: false, &block)
367
+ use_master_key: false, &block)
368
368
  # Handle Parse::Object subclass
369
369
  if class_name.is_a?(Class) && class_name < Parse::Object
370
370
  class_name = class_name.parse_class
@@ -83,7 +83,7 @@ module Parse
83
83
  prefix_parts = []
84
84
  prefix_parts << "request_id=#{request_id}" if request_id
85
85
  prefix_parts << "class=#{class_name}" if class_name
86
- prefixed = prefix_parts.empty? ? text : "#{prefix_parts.join(' ')} #{text}"
86
+ prefixed = prefix_parts.empty? ? text : "#{prefix_parts.join(" ")} #{text}"
87
87
  super(prefixed)
88
88
  end
89
89
  end
data/lib/parse/lock.rb CHANGED
@@ -93,10 +93,10 @@ module Parse
93
93
  module Lock
94
94
  KEY_PREFIX = "parse-stack:lock:v1:"
95
95
 
96
- DEFAULT_TTL = 3
96
+ DEFAULT_TTL = 3
97
97
  DEFAULT_WAIT = 2.0
98
- MAX_TTL = 30
99
- MAX_WAIT = 30
98
+ MAX_TTL = 30
99
+ MAX_WAIT = 30
100
100
 
101
101
  # Minimum byte-length for an explicit `secret:` kwarg. 16 bytes
102
102
  # ≈ 128 bits of separation between tenants — short enough not to
@@ -196,13 +196,13 @@ module Parse
196
196
  # @raise [Parse::Lock::UnavailableError] when `on_degraded: :raise`
197
197
  # and the store is process-local.
198
198
  def acquire(key, ttl: DEFAULT_TTL, wait: DEFAULT_WAIT,
199
- on_degraded: :warn, secret: :auto, &block)
199
+ on_degraded: :warn, secret: :auto, &block)
200
200
  raise ArgumentError, "block required" unless block_given?
201
201
  validated_key = validate_key!(key)
202
202
  validate_on_degraded!(on_degraded)
203
203
  validate_secret!(secret)
204
- normalized_ttl = clamp(Integer(ttl), 1, MAX_TTL)
205
- normalized_wait = clamp(Float(wait), 0.0, MAX_WAIT)
204
+ normalized_ttl = clamp(Integer(ttl), 1, MAX_TTL)
205
+ normalized_wait = clamp(Float(wait), 0.0, MAX_WAIT)
206
206
 
207
207
  # Route through Parse::LockBackend — the shared module that
208
208
  # also serves Parse::CreateLock. The KEY_PREFIX
@@ -216,15 +216,12 @@ module Parse
216
216
  # up the operator-configured secret if one exists; the
217
217
  # explicit-String branch overrides it; the explicit-nil
218
218
  # branch opts out without a warn.
219
- resolved_secret =
220
- case secret
221
- when :auto then Parse::LockBackend.lock_secret_for(store: store, source: "Parse::Lock")
222
- when String then secret
223
- when nil then nil
219
+ resolved_secret = case secret
220
+ when :auto then Parse::LockBackend.lock_secret_for(store: store, source: "Parse::Lock")
221
+ when String then secret
222
+ when nil then nil
224
223
  end
225
- digest = resolved_secret \
226
- ? OpenSSL::HMAC.hexdigest("SHA256", resolved_secret, validated_key) \
227
- : Digest::SHA256.hexdigest(validated_key)
224
+ digest = resolved_secret ? OpenSSL::HMAC.hexdigest("SHA256", resolved_secret, validated_key) : Digest::SHA256.hexdigest(validated_key)
228
225
  store_key = "#{KEY_PREFIX}#{digest}"
229
226
 
230
227
  if Parse::LockBackend.degraded_store?(store)
@@ -238,13 +235,13 @@ module Parse
238
235
  # cross-process owner here — the Mutex IS the exclusion — so a
239
236
  # fresh UUID is purely for signature parity / local fencing.
240
237
  return Parse::LockBackend.synchronize_process_mutex(store_key) do
241
- yield SecureRandom.uuid
242
- end
238
+ yield SecureRandom.uuid
239
+ end
243
240
  end
244
241
 
245
- owner = SecureRandom.uuid
242
+ owner = SecureRandom.uuid
246
243
  acquired_at = nil
247
- start = Parse::LockBackend.monotonic_now
244
+ start = Parse::LockBackend.monotonic_now
248
245
 
249
246
  loop do
250
247
  if Parse::LockBackend.try_acquire(store, store_key, owner, normalized_ttl)
@@ -102,7 +102,7 @@ module Parse
102
102
  # error (Parse::CreateLockUnavailableError vs
103
103
  # Parse::Lock::UnavailableError) without coupling here.
104
104
  def handle_degraded(mode, key, source: "Parse::LockBackend",
105
- unavailable_error: nil)
105
+ unavailable_error: nil)
106
106
  case mode
107
107
  when :raise
108
108
  err = unavailable_error || Parse::Error
@@ -236,9 +236,9 @@ module Parse
236
236
  mongo_local = "_p_#{pointer_field}"
237
237
  if foreign_has_parse_reference?(target_class)
238
238
  replace_keys(spec,
239
- "from" => from_collection,
240
- "localField" => mongo_local,
241
- "foreignField" => PARSE_REFERENCE_REMOTE)
239
+ "from" => from_collection,
240
+ "localField" => mongo_local,
241
+ "foreignField" => PARSE_REFERENCE_REMOTE)
242
242
  else
243
243
  as_value = read_string(spec, "as")
244
244
  let_var = "rwLookupId_#{pointer_field}"
@@ -262,9 +262,9 @@ module Parse
262
262
  mongo_foreign = "_p_#{pointer_field}"
263
263
  if foreign_has_parse_reference?(local_class)
264
264
  replace_keys(spec,
265
- "from" => from_collection,
266
- "localField" => PARSE_REFERENCE_REMOTE,
267
- "foreignField" => mongo_foreign)
265
+ "from" => from_collection,
266
+ "localField" => PARSE_REFERENCE_REMOTE,
267
+ "foreignField" => mongo_foreign)
268
268
  else
269
269
  as_value = read_string(spec, "as")
270
270
  let_var = "rwReverseId_#{pointer_field}"