parse-stack-next 4.5.0 → 5.0.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.
- checksums.yaml +4 -4
- data/.env.sample +17 -3
- data/.github/workflows/codeql.yml +44 -0
- data/.github/workflows/docs.yml +39 -0
- data/.github/workflows/ruby.yml +8 -6
- data/.gitignore +4 -0
- data/CHANGELOG.md +295 -72
- data/Gemfile.lock +10 -3
- data/LICENSE.txt +1 -1
- data/README.md +190 -219
- data/Rakefile +1 -1
- data/SECURITY.md +30 -0
- data/assets/parse-stack-next-avatar.png +0 -0
- data/assets/parse-stack-next-avatar.svg +37 -0
- data/assets/parse-stack-next-banner.png +0 -0
- data/assets/parse-stack-next-banner.svg +45 -0
- data/assets/parse-stack-next-social-preview.png +0 -0
- data/docs/atlas_vector_search_guide.md +511 -0
- data/docs/client_sdk_guide.md +1320 -0
- data/docs/mcp_guide.md +225 -104
- data/docs/mongodb_direct_guide.md +21 -4
- data/docs/usage_guide.md +585 -0
- data/examples/transaction_example.rb +28 -28
- data/lib/parse/acl_scope.rb +2 -2
- data/lib/parse/agent/mcp_rack_app.rb +184 -16
- data/lib/parse/agent/metadata_dsl.rb +16 -16
- data/lib/parse/agent/pipeline_validator.rb +28 -1
- data/lib/parse/agent/prompts.rb +5 -5
- data/lib/parse/agent/tools.rb +287 -14
- data/lib/parse/agent.rb +209 -12
- data/lib/parse/api/analytics.rb +27 -5
- data/lib/parse/api/files.rb +6 -2
- data/lib/parse/api/push.rb +21 -4
- data/lib/parse/api/server.rb +59 -0
- data/lib/parse/api/users.rb +26 -2
- data/lib/parse/atlas_search/index_manager.rb +84 -0
- data/lib/parse/atlas_search.rb +37 -9
- data/lib/parse/cache/pool.rb +73 -0
- data/lib/parse/cache/redis.rb +190 -0
- data/lib/parse/client/body_builder.rb +94 -0
- data/lib/parse/client/caching.rb +109 -9
- data/lib/parse/client/response.rb +27 -0
- data/lib/parse/client.rb +74 -3
- data/lib/parse/console.rb +203 -0
- data/lib/parse/embeddings/cohere.rb +484 -0
- data/lib/parse/embeddings/fixture.rb +130 -0
- data/lib/parse/embeddings/jina.rb +454 -0
- data/lib/parse/embeddings/local_http.rb +492 -0
- data/lib/parse/embeddings/openai.rb +520 -0
- data/lib/parse/embeddings/provider.rb +264 -0
- data/lib/parse/embeddings/qwen.rb +431 -0
- data/lib/parse/embeddings/voyage.rb +550 -0
- data/lib/parse/embeddings.rb +225 -0
- data/lib/parse/graphql/scalars.rb +53 -0
- data/lib/parse/graphql/type_generator.rb +264 -0
- data/lib/parse/graphql.rb +48 -0
- data/lib/parse/live_query/client.rb +24 -5
- data/lib/parse/live_query/subscription.rb +17 -6
- data/lib/parse/live_query.rb +9 -4
- data/lib/parse/model/associations/collection_proxy.rb +2 -2
- data/lib/parse/model/associations/has_many.rb +32 -1
- data/lib/parse/model/associations/has_one.rb +17 -0
- data/lib/parse/model/associations/pointer_collection_proxy.rb +3 -3
- data/lib/parse/model/classes/user.rb +307 -11
- data/lib/parse/model/clp.rb +1 -1
- data/lib/parse/model/core/embed_managed.rb +296 -0
- data/lib/parse/model/core/fetching.rb +4 -4
- data/lib/parse/model/core/indexing.rb +53 -14
- data/lib/parse/model/core/parse_reference.rb +3 -3
- data/lib/parse/model/core/properties.rb +70 -1
- data/lib/parse/model/core/querying.rb +57 -1
- data/lib/parse/model/core/vector_searchable.rb +285 -0
- data/lib/parse/model/file.rb +16 -4
- data/lib/parse/model/model.rb +26 -10
- data/lib/parse/model/object.rb +63 -6
- data/lib/parse/model/pointer.rb +16 -2
- data/lib/parse/model/shortnames.rb +2 -0
- data/lib/parse/model/validations/uniqueness_validator.rb +3 -3
- data/lib/parse/model/vector.rb +102 -0
- data/lib/parse/mongodb.rb +90 -8
- data/lib/parse/pipeline_security.rb +59 -2
- data/lib/parse/query/constraints.rb +16 -14
- data/lib/parse/query/ordering.rb +1 -1
- data/lib/parse/query.rb +137 -64
- data/lib/parse/stack/generators/templates/model.erb +2 -2
- data/lib/parse/stack/generators/templates/model_installation.rb +1 -1
- data/lib/parse/stack/generators/templates/model_role.rb +1 -1
- data/lib/parse/stack/generators/templates/model_session.rb +1 -1
- data/lib/parse/stack/generators/templates/parse.rb +1 -1
- data/lib/parse/stack/generators/templates/webhooks.rb +1 -1
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +375 -73
- data/lib/parse/two_factor_auth/user_extension.rb +5 -2
- data/lib/parse/vector_search.rb +341 -0
- data/parse-stack-next.gemspec +10 -9
- data/scripts/docker/docker-compose.test.yml +18 -0
- data/scripts/start-parse.sh +6 -0
- data/scripts/vector_prototype/create_vector_index.js +105 -0
- data/scripts/vector_prototype/fetch_embeddings.py +241 -0
- data/scripts/vector_prototype/fixture_manifest.json +9 -0
- data/scripts/vector_prototype/query_prototype.rb +84 -0
- data/scripts/vector_prototype/run.sh +34 -0
- metadata +75 -5
- data/parse-stack.png +0 -0
|
@@ -33,6 +33,14 @@ module Parse
|
|
|
33
33
|
ERROR_EMAIL_NOT_FOUND = 205
|
|
34
34
|
# Code when the email is invalid
|
|
35
35
|
ERROR_EMAIL_INVALID = 125
|
|
36
|
+
# Code returned for invalid or expired session tokens (Parse Server).
|
|
37
|
+
ERROR_INVALID_SESSION_TOKEN = 209
|
|
38
|
+
# Code returned for operations that are not permitted under the
|
|
39
|
+
# caller's ACL / CLP / authentication scope. Parse Server uses
|
|
40
|
+
# this code for both "you are not authenticated for this" and
|
|
41
|
+
# "you are authenticated, but not authorized" — see
|
|
42
|
+
# {#permission_denied?} for the SDK-friendly predicate.
|
|
43
|
+
ERROR_OPERATION_FORBIDDEN = 119
|
|
36
44
|
|
|
37
45
|
# The field name for the error.
|
|
38
46
|
ERROR = "error".freeze
|
|
@@ -171,6 +179,25 @@ module Parse
|
|
|
171
179
|
@code == ERROR_OBJECT_NOT_FOUND
|
|
172
180
|
end
|
|
173
181
|
|
|
182
|
+
# true if the response indicates the caller is not authorized to
|
|
183
|
+
# perform the requested operation. Parse Server signals authorization
|
|
184
|
+
# failure in two shapes that no-master-key clients commonly hit:
|
|
185
|
+
#
|
|
186
|
+
# - HTTP 403 with no body (sometimes 401) — recorded as
|
|
187
|
+
# `http_status` only, with no error code in the JSON.
|
|
188
|
+
# - HTTP 400 + error code 119 (`OPERATION_FORBIDDEN`) — typical for
|
|
189
|
+
# CLP and `protectedFields` denials.
|
|
190
|
+
# - HTTP 400 + error code 209 (`INVALID_SESSION_TOKEN`) — session
|
|
191
|
+
# token missing, revoked, or expired.
|
|
192
|
+
#
|
|
193
|
+
# This predicate normalizes those into a single check so client code
|
|
194
|
+
# doesn't have to remember both the HTTP-status and code-only paths.
|
|
195
|
+
# @return [Boolean]
|
|
196
|
+
def permission_denied?
|
|
197
|
+
return true if @http_status == 401 || @http_status == 403
|
|
198
|
+
@code == ERROR_OPERATION_FORBIDDEN || @code == ERROR_INVALID_SESSION_TOKEN
|
|
199
|
+
end
|
|
200
|
+
|
|
174
201
|
# @return [Array] the result data from the response.
|
|
175
202
|
def results
|
|
176
203
|
return [] if @result.nil?
|
data/lib/parse/client.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "faraday"
|
|
2
4
|
|
|
3
5
|
# Attempt to load the persistent connection adapter for better performance.
|
|
@@ -29,6 +31,7 @@ require_relative "client/batch"
|
|
|
29
31
|
require_relative "client/body_builder"
|
|
30
32
|
require_relative "client/authentication"
|
|
31
33
|
require_relative "client/caching"
|
|
34
|
+
require_relative "cache/redis"
|
|
32
35
|
require_relative "client/logging"
|
|
33
36
|
require_relative "client/profiling"
|
|
34
37
|
require_relative "api/all"
|
|
@@ -392,10 +395,13 @@ module Parse
|
|
|
392
395
|
# Parse.setup(
|
|
393
396
|
# connection_pooling: { pool_size: 5, idle_timeout: 60, keep_alive: 60 }
|
|
394
397
|
# )
|
|
395
|
-
# @option opts [Moneta::Transformer,Moneta::Expires] :cache A caching adapter of type
|
|
398
|
+
# @option opts [Moneta::Transformer,Moneta::Expires,Parse::Cache::Redis,String] :cache A caching adapter of type
|
|
396
399
|
# {https://github.com/minad/moneta Moneta::Transformer} or
|
|
397
400
|
# {https://github.com/minad/moneta Moneta::Expires} that will be used
|
|
398
|
-
# by the caching middleware {Parse::Middleware::Caching}.
|
|
401
|
+
# by the caching middleware {Parse::Middleware::Caching}. You can also
|
|
402
|
+
# pass a `redis://` URL string (an internal Moneta-Redis store will be
|
|
403
|
+
# built for you) or a {Parse::Cache::Redis} wrapper, which adds a
|
|
404
|
+
# connection pool and automatic namespace forwarding.
|
|
399
405
|
# Caching queries and object fetches can help improve the performance of
|
|
400
406
|
# your application, even if it is for a few seconds. Only successful GET
|
|
401
407
|
# object fetches and non-empty result queries will be cached by default.
|
|
@@ -407,6 +413,13 @@ module Parse
|
|
|
407
413
|
# middleware. The default value is 3 seconds. If :expires is set to 0,
|
|
408
414
|
# caching will be disabled. You can always clear the current state of the
|
|
409
415
|
# cache using the clear_cache! method on your Parse::Client instance.
|
|
416
|
+
# @option opts [String] :cache_namespace Optional prefix applied to every
|
|
417
|
+
# cache key. Useful when two Parse apps share one Redis instance and
|
|
418
|
+
# would otherwise collide on identical paths (e.g.
|
|
419
|
+
# `mk:/classes/Song/abc`). Keys become `<namespace>:<existing-prefix>:<url>`,
|
|
420
|
+
# so a `SCAN <namespace>:*` evicts a whole app cleanly. Defaults to no
|
|
421
|
+
# namespace for backward compatibility — explicit only, never auto-derived
|
|
422
|
+
# from `app_id`.
|
|
410
423
|
# @option opts [Hash] :faraday You may pass a hash of options that will be
|
|
411
424
|
# passed to the Faraday constructor.
|
|
412
425
|
# @option opts [String] :live_query_url The WebSocket URL for Parse LiveQuery server
|
|
@@ -565,8 +578,23 @@ module Parse
|
|
|
565
578
|
unless [:key?, :[], :delete, :store].all? { |method| opts[:cache].respond_to?(method) }
|
|
566
579
|
raise ArgumentError, "Parse::Client option :cache needs to be a type of Moneta store"
|
|
567
580
|
end
|
|
581
|
+
|
|
582
|
+
# If the caller passed a `Parse::Cache::Redis` wrapper, let its
|
|
583
|
+
# built-in namespace flow through automatically. An explicit
|
|
584
|
+
# `cache_namespace:` still wins so callers can override.
|
|
585
|
+
if defined?(Parse::Cache::Redis) && opts[:cache].is_a?(Parse::Cache::Redis)
|
|
586
|
+
opts[:cache_namespace] ||= opts[:cache].namespace
|
|
587
|
+
end
|
|
588
|
+
|
|
568
589
|
self.cache = opts[:cache]
|
|
569
|
-
conn.use Parse::Middleware::Caching, self.cache, {
|
|
590
|
+
conn.use Parse::Middleware::Caching, self.cache, {
|
|
591
|
+
expires: opts[:expires].to_i,
|
|
592
|
+
# Optional `cache_namespace:` prefixes every key so two Parse
|
|
593
|
+
# apps sharing one Redis don't collide on `mk:/classes/Song/abc`.
|
|
594
|
+
# Explicit only — we do NOT auto-derive from app_id to keep
|
|
595
|
+
# existing single-app deployments backward-compatible.
|
|
596
|
+
namespace: opts[:cache_namespace],
|
|
597
|
+
}
|
|
570
598
|
|
|
571
599
|
# Inform about opt-in cache behavior
|
|
572
600
|
unless Parse.default_query_cache
|
|
@@ -738,6 +766,29 @@ module Parse
|
|
|
738
766
|
# @see Parse::Protocol
|
|
739
767
|
# @see Parse::Request
|
|
740
768
|
def request(method, uri = nil, body: nil, query: nil, headers: nil, opts: {})
|
|
769
|
+
# Pre-declare locals referenced inside rescue blocks so CodeQL's
|
|
770
|
+
# uninitialized-variable analysis is satisfied even if an exception
|
|
771
|
+
# raises before the natural assignment site.
|
|
772
|
+
response = nil
|
|
773
|
+
_retry_count = nil
|
|
774
|
+
_retry_delay = nil
|
|
775
|
+
_request = nil
|
|
776
|
+
# Kwarg-absorption guard. The `**opts` splat in API helper methods
|
|
777
|
+
# (lib/parse/api/*.rb) absorbs a caller-passed `opts: { ... }`
|
|
778
|
+
# keyword as a key named `:opts` rather than as the request options
|
|
779
|
+
# hash itself. The auth context (session_token, use_master_key)
|
|
780
|
+
# buried under :opts then never reaches the request — the call
|
|
781
|
+
# silently goes out anonymous (or master, if one is configured),
|
|
782
|
+
# which is a permission-downgrade footgun. Fail loudly here so the
|
|
783
|
+
# bug surfaces in dev/test instead of in production.
|
|
784
|
+
if opts.is_a?(Hash) && opts[:opts].is_a?(Hash)
|
|
785
|
+
raise ArgumentError, "Parse::Client#request received nested `opts: { opts: { ... } }` — " \
|
|
786
|
+
"pass session_token: / use_master_key: directly as keywords, " \
|
|
787
|
+
"not wrapped in an `opts:` hash. " \
|
|
788
|
+
"Bad: Parse.client.create_object('X', body, opts: { session_token: t }) " \
|
|
789
|
+
"Good: Parse.client.create_object('X', body, session_token: t, use_master_key: false)"
|
|
790
|
+
end
|
|
791
|
+
|
|
741
792
|
_retry_count ||= self.retry_limit
|
|
742
793
|
|
|
743
794
|
if opts[:retry] == false
|
|
@@ -776,11 +827,31 @@ module Parse
|
|
|
776
827
|
headers[Parse::Middleware::Caching::CACHE_EXPIRES_DURATION] = opts[:cache].to_s
|
|
777
828
|
end
|
|
778
829
|
|
|
830
|
+
# Resolve the auth context in three layers:
|
|
831
|
+
# 1. explicit per-call `use_master_key:` and `session_token:`
|
|
832
|
+
# 2. ambient session set by `Parse.with_session { ... }` (fiber-local)
|
|
833
|
+
# 3. process-wide `Parse.client_mode` flag — when true, master key is
|
|
834
|
+
# never sent unless the caller explicitly passed `use_master_key: true`
|
|
835
|
+
explicit_master = opts.key?(:use_master_key)
|
|
836
|
+
|
|
779
837
|
if opts[:use_master_key] == false
|
|
780
838
|
headers[Parse::Middleware::Authentication::DISABLE_MASTER_KEY] = "true"
|
|
839
|
+
elsif Parse.client_mode && opts[:use_master_key] != true
|
|
840
|
+
# client mode defaults master key OFF unless explicitly opted in
|
|
841
|
+
headers[Parse::Middleware::Authentication::DISABLE_MASTER_KEY] = "true"
|
|
781
842
|
end
|
|
782
843
|
|
|
783
844
|
token = opts[:session_token]
|
|
845
|
+
# When no explicit token was passed AND the caller didn't ask to send
|
|
846
|
+
# the master key, fall through to the fiber-local ambient set by
|
|
847
|
+
# `Parse.with_session`. Explicit `use_master_key: true` is treated as
|
|
848
|
+
# a deliberate admin call and skips the ambient — otherwise an
|
|
849
|
+
# `admin.do_thing(use_master_key: true)` nested inside a
|
|
850
|
+
# `with_session(user)` block would silently downgrade.
|
|
851
|
+
if token.nil? && !(explicit_master && opts[:use_master_key] == true)
|
|
852
|
+
ambient = Parse.current_session_token
|
|
853
|
+
token = ambient if ambient.is_a?(String) && !ambient.empty?
|
|
854
|
+
end
|
|
784
855
|
if token.present?
|
|
785
856
|
token = token.session_token if token.respond_to?(:session_token)
|
|
786
857
|
headers[Parse::Middleware::Authentication::DISABLE_MASTER_KEY] = "true"
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Console-friendly helpers for interactive Parse sessions.
|
|
5
|
+
#
|
|
6
|
+
# `watch` and `wait_for` wrap the LiveQuery subscription machinery in a
|
|
7
|
+
# blocking-by-default shape suited for `bin/rails console`, `bin/console`,
|
|
8
|
+
# or one-off Rake tasks where the caller wants to:
|
|
9
|
+
#
|
|
10
|
+
# - Tail a class as rows arrive ("watch new posts"), Ctrl-C to stop.
|
|
11
|
+
# - Block until a specific row appears or a condition matches
|
|
12
|
+
# ("wait until job N flips to :done"), with an optional timeout.
|
|
13
|
+
#
|
|
14
|
+
# Auth resolution is automatic:
|
|
15
|
+
# - If `Parse.current_session_token` is set (via `Parse.login`,
|
|
16
|
+
# `Parse.with_session`, or `Parse.session_token=`), the subscription
|
|
17
|
+
# is ACL-aware as that user.
|
|
18
|
+
# - Otherwise it falls through with no token — the LiveQuery server
|
|
19
|
+
# then applies whatever default the master-key / unauthenticated
|
|
20
|
+
# subscription model dictates for the class.
|
|
21
|
+
#
|
|
22
|
+
# Both helpers also accept an explicit `session_token:` kwarg for
|
|
23
|
+
# tests / fixtures.
|
|
24
|
+
|
|
25
|
+
require "timeout"
|
|
26
|
+
|
|
27
|
+
module Parse
|
|
28
|
+
module Console
|
|
29
|
+
DEFAULT_WATCH_EVENTS = [:create, :update, :delete, :enter, :leave].freeze
|
|
30
|
+
DEFAULT_WAIT_FOR_EVENTS = [:create, :enter].freeze
|
|
31
|
+
|
|
32
|
+
module_function
|
|
33
|
+
|
|
34
|
+
# Open a LiveQuery subscription on `klass` and block until SIGINT
|
|
35
|
+
# (Ctrl-C). Intended for REPL / console use — emits arriving events
|
|
36
|
+
# to `$stdout` by default, or yields each one to a caller-supplied
|
|
37
|
+
# block.
|
|
38
|
+
#
|
|
39
|
+
# @example Tail every event for a class
|
|
40
|
+
# Parse.watch(Post)
|
|
41
|
+
# # ^C to stop
|
|
42
|
+
#
|
|
43
|
+
# @example Tail with a query and a custom handler
|
|
44
|
+
# Parse.watch(Post, where: { category: "alerts" }) do |event, obj|
|
|
45
|
+
# puts "[#{event}] #{obj.title} (#{obj.id})"
|
|
46
|
+
# end
|
|
47
|
+
#
|
|
48
|
+
# @example Admin-style with master key (no ambient session)
|
|
49
|
+
# Parse.with_session(nil) { Parse.watch(JobRun) }
|
|
50
|
+
#
|
|
51
|
+
# @param klass [Class] a Parse::Object subclass.
|
|
52
|
+
# @param where [Hash] optional query constraints.
|
|
53
|
+
# @param on [Symbol, Array<Symbol>, nil] which event types to
|
|
54
|
+
# subscribe to (default: all of :create/:update/:delete/:enter/:leave).
|
|
55
|
+
# @param fields [Array<String>, nil] only fire updates when these
|
|
56
|
+
# fields change.
|
|
57
|
+
# @param session_token [String, nil] explicit override; defaults to
|
|
58
|
+
# `Parse.current_session_token`.
|
|
59
|
+
# @yield [event, object] called for each emitted event when a block
|
|
60
|
+
# is supplied. `event` is one of the watched symbols; `object` is
|
|
61
|
+
# the row.
|
|
62
|
+
# @return [Integer] the count of events delivered before the caller
|
|
63
|
+
# interrupted (Ctrl-C) or the subscription was torn down.
|
|
64
|
+
def watch(klass, where: {}, on: nil, fields: nil, session_token: nil, &block)
|
|
65
|
+
events = Array(on || DEFAULT_WATCH_EVENTS).map(&:to_sym)
|
|
66
|
+
printer = block_given? ? block : ->(ev, obj) {
|
|
67
|
+
title = obj.respond_to?(:id) ? obj.id : obj.inspect
|
|
68
|
+
puts "[#{Time.now.iso8601}] #{klass.parse_class}.#{ev} #{title}"
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
delivered = 0
|
|
72
|
+
counter_lock = Monitor.new
|
|
73
|
+
sub = _open_subscription(klass, where: where, fields: fields, session_token: session_token)
|
|
74
|
+
|
|
75
|
+
events.each do |ev|
|
|
76
|
+
sub.on(ev) do |obj, _original = nil|
|
|
77
|
+
counter_lock.synchronize { delivered += 1 }
|
|
78
|
+
begin
|
|
79
|
+
printer.call(ev, obj)
|
|
80
|
+
rescue StandardError => e
|
|
81
|
+
warn "[Parse.watch] handler raised #{e.class}: #{e.message}"
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
sub.on(:error) { |err| warn "[Parse.watch] error: #{err}" }
|
|
86
|
+
|
|
87
|
+
_block_until_interrupt
|
|
88
|
+
delivered
|
|
89
|
+
ensure
|
|
90
|
+
sub.unsubscribe if sub && sub.respond_to?(:unsubscribe)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Block until a row matching the predicate arrives via LiveQuery,
|
|
94
|
+
# then return that row. Useful for `wait until the job flips`,
|
|
95
|
+
# `wait until the inbox row lands`, integration tests, etc.
|
|
96
|
+
#
|
|
97
|
+
# By default the first `:create`/`:enter` event resolves the wait.
|
|
98
|
+
# Pass a block to require the event also satisfy a predicate —
|
|
99
|
+
# `wait_for(Job, where: { kind: "import" }) { |j| j.status == "done" }`
|
|
100
|
+
# will keep waiting until both the query matches AND the block
|
|
101
|
+
# returns truthy.
|
|
102
|
+
#
|
|
103
|
+
# @example Wait for the first row of a class
|
|
104
|
+
# first = Parse.wait_for(Notification)
|
|
105
|
+
#
|
|
106
|
+
# @example Wait with a query and a predicate
|
|
107
|
+
# done = Parse.wait_for(Job, where: { kind: "import" },
|
|
108
|
+
# timeout: 60) { |j| j.status == "done" }
|
|
109
|
+
#
|
|
110
|
+
# @param klass [Class] a Parse::Object subclass.
|
|
111
|
+
# @param where [Hash] optional query constraints applied server-side.
|
|
112
|
+
# @param on [Symbol, Array<Symbol>] which event types to count
|
|
113
|
+
# (default: [:create, :enter]; pass :update for status-flip
|
|
114
|
+
# watching).
|
|
115
|
+
# @param timeout [Numeric, nil] seconds to wait. nil = forever.
|
|
116
|
+
# @param fields [Array<String>, nil] field filter for update events.
|
|
117
|
+
# @param session_token [String, nil] explicit override; defaults to
|
|
118
|
+
# `Parse.current_session_token`.
|
|
119
|
+
# @yield [object] optional predicate; must return truthy to resolve.
|
|
120
|
+
# @return [Parse::Object] the matched row.
|
|
121
|
+
# @raise [Timeout::Error] when `timeout:` elapses with no match.
|
|
122
|
+
def wait_for(klass, where: {}, on: nil, timeout: nil, fields: nil,
|
|
123
|
+
session_token: nil, &predicate)
|
|
124
|
+
events = Array(on || DEFAULT_WAIT_FOR_EVENTS).map(&:to_sym)
|
|
125
|
+
queue = Queue.new
|
|
126
|
+
sub = _open_subscription(klass, where: where, fields: fields, session_token: session_token)
|
|
127
|
+
|
|
128
|
+
events.each do |ev|
|
|
129
|
+
sub.on(ev) do |obj, _original = nil|
|
|
130
|
+
begin
|
|
131
|
+
next if predicate && !predicate.call(obj)
|
|
132
|
+
queue << obj
|
|
133
|
+
rescue StandardError => e
|
|
134
|
+
queue << e
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
sub.on(:error) { |err| queue << err }
|
|
139
|
+
|
|
140
|
+
result = if timeout
|
|
141
|
+
Timeout.timeout(timeout) { queue.pop }
|
|
142
|
+
else
|
|
143
|
+
queue.pop
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
raise result if result.is_a?(Exception)
|
|
147
|
+
result
|
|
148
|
+
ensure
|
|
149
|
+
sub.unsubscribe if sub && sub.respond_to?(:unsubscribe)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# @!visibility private
|
|
153
|
+
def _open_subscription(klass, where:, fields:, session_token:)
|
|
154
|
+
unless klass.respond_to?(:subscribe)
|
|
155
|
+
raise ArgumentError, "#{klass.inspect} does not implement .subscribe — pass a Parse::Object subclass"
|
|
156
|
+
end
|
|
157
|
+
klass.subscribe(where: where, fields: fields, session_token: session_token)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# @!visibility private
|
|
161
|
+
#
|
|
162
|
+
# Block the current thread until SIGINT (Ctrl-C) is received.
|
|
163
|
+
# We install a one-shot handler that releases a sleeping queue.pop
|
|
164
|
+
# and restore the prior handler on exit so library users can wrap
|
|
165
|
+
# `watch` inside their own signal-handling code without losing it.
|
|
166
|
+
def _block_until_interrupt
|
|
167
|
+
gate = Queue.new
|
|
168
|
+
prior = Signal.trap("INT") { gate << :interrupted }
|
|
169
|
+
gate.pop
|
|
170
|
+
ensure
|
|
171
|
+
Signal.trap("INT", prior || "DEFAULT") if defined?(prior)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
class << self
|
|
176
|
+
# @see Parse::Console.watch
|
|
177
|
+
def watch(klass, **kwargs, &block)
|
|
178
|
+
Console.watch(klass, **kwargs, &block)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# @see Parse::Console.wait_for
|
|
182
|
+
def wait_for(klass, **kwargs, &block)
|
|
183
|
+
Console.wait_for(klass, **kwargs, &block)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
class Object < Pointer
|
|
188
|
+
class << self
|
|
189
|
+
# Tail this class as LiveQuery events arrive — blocking, Ctrl-C
|
|
190
|
+
# to stop. See {Parse::Console.watch}.
|
|
191
|
+
def watch(**kwargs, &block)
|
|
192
|
+
Parse::Console.watch(self, **kwargs, &block)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Block until the first row matching {where} (and an optional
|
|
196
|
+
# predicate block) arrives via LiveQuery. See
|
|
197
|
+
# {Parse::Console.wait_for}.
|
|
198
|
+
def wait_for(**kwargs, &block)
|
|
199
|
+
Parse::Console.wait_for(self, **kwargs, &block)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|