parse-stack-next 5.3.0 → 5.4.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/.gitignore +2 -0
- data/CHANGELOG.md +461 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +12 -4
- data/README.md +160 -3
- data/Rakefile +52 -3
- data/docs/atlas_vector_search_guide.md +86 -2
- data/docs/client_sdk_guide.md +5 -0
- data/docs/mcp_guide.md +59 -4
- data/docs/mongodb_direct_guide.md +93 -1
- data/docs/usage_guide.md +11 -1
- data/docs/webhooks_guide.md +418 -0
- data/examples/README.md +46 -0
- data/examples/basic_client.rb +93 -0
- data/examples/basic_server.rb +109 -0
- data/examples/live_query_listener.rb +98 -0
- data/examples/rag_chatbot.rb +221 -0
- data/examples/webhook_server.rb +111 -0
- data/lib/parse/agent/mcp_rack_app.rb +285 -62
- data/lib/parse/agent/tools.rb +45 -5
- data/lib/parse/api/aggregate.rb +7 -1
- data/lib/parse/api/cloud_functions.rb +12 -4
- data/lib/parse/api/hooks.rb +46 -9
- data/lib/parse/api/objects.rb +16 -2
- data/lib/parse/api/path_segment.rb +33 -0
- data/lib/parse/api/server.rb +94 -0
- data/lib/parse/api/users.rb +58 -2
- data/lib/parse/atlas_search.rb +7 -7
- data/lib/parse/client/body_builder.rb +5 -0
- data/lib/parse/client/protocol.rb +4 -0
- data/lib/parse/client.rb +55 -2
- data/lib/parse/embeddings/spend_cap.rb +255 -0
- data/lib/parse/embeddings.rb +1 -0
- data/lib/parse/live_query/client.rb +3 -1
- data/lib/parse/live_query/subscription.rb +32 -5
- data/lib/parse/model/acl.rb +4 -2
- data/lib/parse/model/classes/audience.rb +52 -4
- data/lib/parse/model/classes/user.rb +180 -3
- data/lib/parse/model/core/embed_managed.rb +113 -0
- data/lib/parse/model/core/querying.rb +3 -1
- data/lib/parse/model/core/vector_searchable.rb +161 -0
- data/lib/parse/model/object.rb +28 -5
- data/lib/parse/mongodb.rb +7 -1
- data/lib/parse/pipeline_security.rb +5 -3
- data/lib/parse/query/constraints.rb +29 -0
- data/lib/parse/query.rb +265 -27
- data/lib/parse/retrieval/agent_tool.rb +49 -0
- data/lib/parse/retrieval/reranker/cohere.rb +218 -0
- data/lib/parse/retrieval/reranker.rb +157 -0
- data/lib/parse/retrieval/retriever.rb +110 -23
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +17 -0
- data/lib/parse/two_factor_auth/user_extension.rb +123 -31
- data/lib/parse/vector_search/hybrid.rb +578 -0
- data/lib/parse/webhooks/payload.rb +252 -7
- data/lib/parse/webhooks/trigger_audit.rb +502 -0
- data/lib/parse/webhooks.rb +215 -3
- data/scripts/docker/Dockerfile.parse +5 -1
- data/scripts/docker/docker-compose.test.yml +31 -0
- data/scripts/docker/docker-compose.verifyemail.yml +4 -0
- data/scripts/docker/preflight.sh +76 -0
- data/scripts/start-parse.sh +52 -4
- metadata +15 -1
|
@@ -25,7 +25,9 @@ module Parse
|
|
|
25
25
|
original: nil, update: nil,
|
|
26
26
|
query: nil, log: nil,
|
|
27
27
|
objects: nil,
|
|
28
|
-
triggerName: nil
|
|
28
|
+
triggerName: nil,
|
|
29
|
+
event: nil, clients: nil, subscriptions: nil,
|
|
30
|
+
context: nil }.freeze
|
|
29
31
|
include ::ActiveModel::Serializers::JSON
|
|
30
32
|
# @!attribute [rw] master
|
|
31
33
|
# @return [Boolean] whether the master key was used for this request.
|
|
@@ -64,6 +66,31 @@ module Parse
|
|
|
64
66
|
attr_accessor :master, :user, :installation_id, :params, :function_name, :object, :trigger_name
|
|
65
67
|
attr_accessor :query, :log, :objects
|
|
66
68
|
attr_accessor :original, :update, :raw
|
|
69
|
+
# @!attribute [rw] event
|
|
70
|
+
# The LiveQuery event type for an +afterEvent+ trigger -- one of
|
|
71
|
+
# +"create"+, +"enter"+, +"update"+, +"leave"+, or +"delete"+ -- or
|
|
72
|
+
# +"connect"+ for a +beforeConnect+ trigger. +nil+ for every non-
|
|
73
|
+
# LiveQuery trigger. See {#after_event?} / {#before_connect?}.
|
|
74
|
+
# @return [String, nil]
|
|
75
|
+
# @!attribute [rw] clients
|
|
76
|
+
# Connection-global metadata sent on the LiveQuery +beforeConnect+ /
|
|
77
|
+
# +afterEvent+ triggers: the number of currently-connected LiveQuery
|
|
78
|
+
# clients. +nil+ for non-LiveQuery triggers.
|
|
79
|
+
# @return [Integer, nil]
|
|
80
|
+
# @!attribute [rw] subscriptions
|
|
81
|
+
# Connection-global metadata sent on the LiveQuery +beforeConnect+ /
|
|
82
|
+
# +afterEvent+ triggers: the number of active subscriptions. +nil+ for
|
|
83
|
+
# non-LiveQuery triggers.
|
|
84
|
+
# @return [Integer, nil]
|
|
85
|
+
attr_accessor :event, :clients, :subscriptions
|
|
86
|
+
# @!attribute [rw] context
|
|
87
|
+
# The caller-supplied context object threaded from the originating REST
|
|
88
|
+
# write or cloud-function call via the +X-Parse-Cloud-Context+ header.
|
|
89
|
+
# Parse Server includes this as a top-level +context+ key in trigger
|
|
90
|
+
# payloads (beforeSave/afterSave/etc.). Returns a Hash when present, or
|
|
91
|
+
# +nil+ when the originating request carried no context.
|
|
92
|
+
# @return [Hash, nil]
|
|
93
|
+
attr_accessor :context
|
|
67
94
|
# @!attribute [r] session_token
|
|
68
95
|
# The caller's live Parse session token, captured from the incoming
|
|
69
96
|
# webhook payload (`user.sessionToken`) before credentials are scrubbed
|
|
@@ -85,10 +112,24 @@ module Parse
|
|
|
85
112
|
# You would normally never create a {Parse::Webhooks::Payload} object since it is automatically
|
|
86
113
|
# provided to you when using Parse::Webhooks.
|
|
87
114
|
# @see Parse::Webhooks
|
|
88
|
-
|
|
115
|
+
# @param hash [String, Hash] the raw webhook body (JSON string or Hash).
|
|
116
|
+
# @param webhook_class [String, nil] the Parse class name derived from the
|
|
117
|
+
# webhook URL path (`<endpoint>/<triggerName>/<className>`). This is the
|
|
118
|
+
# ONLY authoritative source of the class for beforeFind/afterFind
|
|
119
|
+
# triggers — Parse Server omits `className` from the find payload body
|
|
120
|
+
# entirely (the matched `objects` carry no `className` and there is no
|
|
121
|
+
# top-level one). Threading it here lets `parse_class` resolve (so find
|
|
122
|
+
# triggers route) and lets `:vector` columns be stripped from afterFind
|
|
123
|
+
# `objects`. For save/delete triggers the path className equals the
|
|
124
|
+
# body's, so it is consistent; for functions it is nil.
|
|
125
|
+
def initialize(hash = {}, webhook_class = nil)
|
|
89
126
|
hash = JSON.parse(hash, max_nesting: 20) if hash.is_a?(String)
|
|
90
127
|
hash = Hash[hash.map { |k, v| [k.to_s.underscore.to_sym, v] }]
|
|
91
128
|
@raw = hash
|
|
129
|
+
# Set BEFORE the vector scrub below so the route-derived class is
|
|
130
|
+
# available to strip :vector columns from afterFind objects (whose
|
|
131
|
+
# body carries no className of its own).
|
|
132
|
+
@webhook_class = webhook_class.to_s if webhook_class && !webhook_class.to_s.empty?
|
|
92
133
|
@master = hash[:master]
|
|
93
134
|
# Capture the caller's session token from the *unscrubbed* user hash
|
|
94
135
|
# before scrub_credentials strips it below. Parse Server includes
|
|
@@ -99,6 +140,16 @@ module Parse
|
|
|
99
140
|
# still letting a handler opt in to acting as the calling user via
|
|
100
141
|
# #session_token / #user_client / #user_agent.
|
|
101
142
|
@session_token = self.class.extract_session_token(hash[:user])
|
|
143
|
+
# LiveQuery beforeConnect/beforeSubscribe carry the caller's session
|
|
144
|
+
# token at the TOP LEVEL (not nested under `user`), because no user is
|
|
145
|
+
# resolved yet when the trigger fires. Capture it here -- with the same
|
|
146
|
+
# "set it aside, keep it out of as_json / the log" treatment as the
|
|
147
|
+
# nested form -- so #user_client / #user_agent can act as the caller.
|
|
148
|
+
# It is intentionally NOT one of ATTRIBUTES.
|
|
149
|
+
if @session_token.nil?
|
|
150
|
+
top_token = hash[:session_token].to_s.strip
|
|
151
|
+
@session_token = top_token unless top_token.empty?
|
|
152
|
+
end
|
|
102
153
|
# Webhook trigger payloads (beforeSave/afterSave/etc.) are delivered by
|
|
103
154
|
# Parse Server and, when a webhook key is configured (the default; see
|
|
104
155
|
# Parse::Webhooks.allow_unauthenticated for the opt-out used in tests /
|
|
@@ -125,14 +176,42 @@ module Parse
|
|
|
125
176
|
@params = hash[:params]
|
|
126
177
|
@params = @params.with_indifferent_access if @params.is_a?(Hash)
|
|
127
178
|
@function_name = hash[:function_name]
|
|
128
|
-
@object = self.class.scrub_credentials(hash[:object])
|
|
129
179
|
@trigger_name = hash[:trigger_name]
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
#
|
|
180
|
+
# Resolve the model class once so :vector columns can be stripped from
|
|
181
|
+
# every object-shaped payload (see scrub_vector_columns). Credentials
|
|
182
|
+
# are scrubbed first, then vectors. The route-derived @webhook_class is
|
|
183
|
+
# authoritative and preferred — it is the only class source for
|
|
184
|
+
# afterFind (whose body carries no className anywhere); for save/delete
|
|
185
|
+
# it equals the body's className. Falls back to the object/original
|
|
186
|
+
# className for older callers that don't supply a route class.
|
|
187
|
+
vec_klass = self.class.resolve_klass_by_name(@webhook_class) ||
|
|
188
|
+
self.class.resolve_vector_klass(hash[:object], hash[:original])
|
|
189
|
+
@object = self.class.scrub_vector_columns(self.class.scrub_credentials(hash[:object]), vec_klass)
|
|
190
|
+
@original = self.class.scrub_vector_columns(self.class.scrub_credentials(hash[:original]), vec_klass)
|
|
191
|
+
@update = self.class.scrub_vector_columns(self.class.scrub_credentials(hash[:update]), vec_klass) || {}
|
|
192
|
+
# Added for beforeFind and afterFind triggers. afterFind objects are all
|
|
193
|
+
# of one class but carry no className of their own, so the route-derived
|
|
194
|
+
# vec_klass is the only way to strip their :vector columns.
|
|
133
195
|
@query = hash[:query]
|
|
134
|
-
|
|
196
|
+
# LiveQuery connection metadata. `event` is the afterEvent event type
|
|
197
|
+
# (create/enter/update/leave/delete) or "connect" for beforeConnect;
|
|
198
|
+
# `clients`/`subscriptions` are connection-global counts. All nil for
|
|
199
|
+
# the object / auth triggers. These are plain scalars (no credential
|
|
200
|
+
# material), so they pass through unscrubbed.
|
|
201
|
+
@event = hash[:event]
|
|
202
|
+
@clients = hash[:clients]
|
|
203
|
+
@subscriptions = hash[:subscriptions]
|
|
204
|
+
@objects = Array(hash[:objects]).map do |o|
|
|
205
|
+
self.class.scrub_vector_columns(self.class.scrub_credentials(o), vec_klass)
|
|
206
|
+
end
|
|
135
207
|
@log = hash[:log]
|
|
208
|
+
# Caller-supplied context object threaded via X-Parse-Cloud-Context.
|
|
209
|
+
# This is caller metadata (not a credential), so it passes through
|
|
210
|
+
# without scrubbing — mirroring the treatment of @query and @log.
|
|
211
|
+
@context = hash[:context]
|
|
212
|
+
# Blocks registered by a handler via #after_response / #defer, to run
|
|
213
|
+
# after the webhook response has been sent (drained by the Rack app).
|
|
214
|
+
@deferred_callbacks = []
|
|
136
215
|
end
|
|
137
216
|
|
|
138
217
|
# @!visibility private
|
|
@@ -166,6 +245,62 @@ module Parse
|
|
|
166
245
|
end
|
|
167
246
|
end
|
|
168
247
|
|
|
248
|
+
# @!visibility private
|
|
249
|
+
# Resolve the Parse::Object subclass for a webhook payload from the
|
|
250
|
+
# `className` of the first object-shaped hash given. Returns nil when
|
|
251
|
+
# no class name is present or no matching model is registered (the
|
|
252
|
+
# caller then skips vector stripping — fail-open is acceptable here:
|
|
253
|
+
# an unregistered class has no declared `:vector` columns to strip).
|
|
254
|
+
def self.resolve_vector_klass(*candidates)
|
|
255
|
+
candidates.each do |obj|
|
|
256
|
+
next unless obj.is_a?(Hash)
|
|
257
|
+
name = obj["className"] || obj[:className]
|
|
258
|
+
next if name.nil? || name.to_s.empty?
|
|
259
|
+
klass = resolve_klass_by_name(name)
|
|
260
|
+
return klass if klass
|
|
261
|
+
end
|
|
262
|
+
nil
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# @!visibility private
|
|
266
|
+
# Resolve a registered Parse::Object subclass from a bare class-name
|
|
267
|
+
# string (e.g. the route-derived @webhook_class). Returns nil for a blank
|
|
268
|
+
# name or an unregistered class (the caller then skips vector stripping —
|
|
269
|
+
# fail-open, as an unregistered class has no declared :vector columns).
|
|
270
|
+
def self.resolve_klass_by_name(name)
|
|
271
|
+
return nil if name.nil? || name.to_s.empty?
|
|
272
|
+
klass = (Parse::Object.find_class(name.to_s) rescue nil)
|
|
273
|
+
klass.respond_to?(:fields) ? klass : nil
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# @!visibility private
|
|
277
|
+
# Returns a copy of +obj+ with the model's declared `:vector`
|
|
278
|
+
# columns removed. Embeddings are large dense float arrays that leak
|
|
279
|
+
# ML signal; a webhook handler has no reason to receive them, and
|
|
280
|
+
# leaving them in bloats logs and any object a handler re-persists.
|
|
281
|
+
# Mirrors the `as_json` default (vectors omitted) — a class that opts
|
|
282
|
+
# into `vector_visibility :public` keeps its vectors here too.
|
|
283
|
+
#
|
|
284
|
+
# `klass` may be passed explicitly (so changed-only payloads like
|
|
285
|
+
# `update`, which carry no `className`, are still scrubbed using the
|
|
286
|
+
# class resolved from the sibling `object`/`original` hash); when nil
|
|
287
|
+
# it is resolved from the hash's own `className`.
|
|
288
|
+
# Pass-through for non-Hash input (and nil).
|
|
289
|
+
def self.scrub_vector_columns(obj, klass = nil)
|
|
290
|
+
return obj unless obj.is_a?(Hash)
|
|
291
|
+
klass ||= resolve_vector_klass(obj)
|
|
292
|
+
return obj if klass.nil?
|
|
293
|
+
if klass.respond_to?(:vectors_public_by_default?) && klass.vectors_public_by_default?
|
|
294
|
+
return obj
|
|
295
|
+
end
|
|
296
|
+
vector_fields = klass.fields(:vector).keys.map(&:to_s)
|
|
297
|
+
return obj if vector_fields.empty?
|
|
298
|
+
field_map = klass.respond_to?(:field_map) ? klass.field_map : {}
|
|
299
|
+
wire = vector_fields.map { |f| (field_map[f.to_sym] || f).to_s }
|
|
300
|
+
denied = (vector_fields + wire)
|
|
301
|
+
obj.reject { |k, _| denied.include?(k.to_s) }
|
|
302
|
+
end
|
|
303
|
+
|
|
169
304
|
# @!visibility private
|
|
170
305
|
# Pulls the caller's session token out of the (unscrubbed) +user+ hash.
|
|
171
306
|
# Parse Server sends it as the camelCase string key +sessionToken+; this
|
|
@@ -325,6 +460,73 @@ module Parse
|
|
|
325
460
|
trigger? && @trigger_name.to_sym == :afterFind
|
|
326
461
|
end
|
|
327
462
|
|
|
463
|
+
# true if this is a beforeLogin webhook trigger request.
|
|
464
|
+
#
|
|
465
|
+
# NOTE: a +beforeLogin+ payload carries the user being authenticated as
|
|
466
|
+
# {#object} / {#parse_object} (a +_User+), NOT as {#user} -- the caller is
|
|
467
|
+
# not yet authenticated when the trigger fires, so {#user} is +nil+. (By
|
|
468
|
+
# +afterLogin+ both are populated and equal.) Reach for {#parse_object} to
|
|
469
|
+
# inspect the logging-in user during +beforeLogin+.
|
|
470
|
+
def before_login?
|
|
471
|
+
trigger? && @trigger_name.to_sym == :beforeLogin
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
# true if this is a afterLogin webhook trigger request.
|
|
475
|
+
def after_login?
|
|
476
|
+
trigger? && @trigger_name.to_sym == :afterLogin
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
# true if this is a afterLogout webhook trigger request. The logged-out
|
|
480
|
+
# session is carried as {#object} / {#parse_object} (a +_Session+).
|
|
481
|
+
def after_logout?
|
|
482
|
+
trigger? && @trigger_name.to_sym == :afterLogout
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
# true if this is a beforePasswordResetRequest webhook trigger request.
|
|
486
|
+
# The target user is carried as {#object} / {#parse_object} (a +_User+).
|
|
487
|
+
def before_password_reset_request?
|
|
488
|
+
trigger? && @trigger_name.to_sym == :beforePasswordResetRequest
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
# true if this is a LiveQuery beforeConnect webhook trigger request.
|
|
492
|
+
# Connection-global: carries no {#object}; the className is the +@Connect+
|
|
493
|
+
# sentinel and the caller's token (if any) is in {#session_token}.
|
|
494
|
+
def before_connect?
|
|
495
|
+
trigger? && @trigger_name.to_sym == :beforeConnect
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
# true if this is a LiveQuery beforeSubscribe webhook trigger request.
|
|
499
|
+
# Shaped like beforeFind: carries a {#query} (see {#parse_query}) and the
|
|
500
|
+
# className comes from the request path, not the body.
|
|
501
|
+
def before_subscribe?
|
|
502
|
+
trigger? && @trigger_name.to_sym == :beforeSubscribe
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
# true if this is a LiveQuery afterEvent webhook trigger request. The
|
|
506
|
+
# event type (create/enter/update/leave/delete) is in {#event}.
|
|
507
|
+
def after_event?
|
|
508
|
+
trigger? && @trigger_name.to_sym == :afterEvent
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
# true if this is one of the authentication-side triggers
|
|
512
|
+
# (beforeLogin / afterLogin / afterLogout / beforePasswordResetRequest).
|
|
513
|
+
# These carry a +_User+ / +_Session+ as {#object} but are NOT object
|
|
514
|
+
# save/delete triggers: no ActiveModel save/create/destroy callbacks run
|
|
515
|
+
# for them, and Parse Server ignores the response body (the only way to
|
|
516
|
+
# affect a +before*+ one is to deny it -- see the webhook router).
|
|
517
|
+
def auth_trigger?
|
|
518
|
+
before_login? || after_login? || after_logout? || before_password_reset_request?
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
# true if this is one of the LiveQuery triggers (beforeConnect /
|
|
522
|
+
# beforeSubscribe / afterEvent). Parse Server delivers these over an HTTP
|
|
523
|
+
# webhook only in a co-located single-process LiveQuery setup;
|
|
524
|
+
# +beforeConnect+ in particular carries a live client and is effectively
|
|
525
|
+
# in-process-only. See the webhooks guide.
|
|
526
|
+
def live_query_trigger?
|
|
527
|
+
before_connect? || before_subscribe? || after_event?
|
|
528
|
+
end
|
|
529
|
+
|
|
328
530
|
# true if this request is a trigger that contains an object.
|
|
329
531
|
def object?
|
|
330
532
|
trigger? && @object.present?
|
|
@@ -483,6 +685,49 @@ module Parse
|
|
|
483
685
|
raise Parse::Webhooks::ResponseError, msg
|
|
484
686
|
end
|
|
485
687
|
|
|
688
|
+
# Register a block to run **after** this webhook's response has been sent
|
|
689
|
+
# to Parse Server, off the client's critical path. Use it to do work that
|
|
690
|
+
# should not add latency to the save/function the client is waiting on —
|
|
691
|
+
# search indexing, cache warming, fan-out notifications.
|
|
692
|
+
#
|
|
693
|
+
# The handler still returns its value synchronously (the response Parse
|
|
694
|
+
# Server acts on); the deferred block runs afterward. When the SDK is
|
|
695
|
+
# mounted under a server that supports `rack.after_reply` (Puma, Unicorn)
|
|
696
|
+
# the block runs once the response is flushed to the socket, on the same
|
|
697
|
+
# worker thread; otherwise it runs in a detached thread. Each block is
|
|
698
|
+
# isolated, so one raising neither affects the response nor the others.
|
|
699
|
+
#
|
|
700
|
+
# Parse::Webhooks.route :after_save, :Post do
|
|
701
|
+
# post = parse_object
|
|
702
|
+
# after_response { SearchIndex.reindex(post.id) }
|
|
703
|
+
# post
|
|
704
|
+
# end
|
|
705
|
+
#
|
|
706
|
+
# `self` inside the block is this payload (it closes over the handler's
|
|
707
|
+
# scope), so `parse_object`, `params`, etc. remain available. Note the
|
|
708
|
+
# block runs in-process and does not survive a worker restart — for work
|
|
709
|
+
# that *must* happen, hand it to a durable job queue instead. Deferred
|
|
710
|
+
# callbacks fire only when the payload is processed through the mounted
|
|
711
|
+
# `Parse::Webhooks` Rack app.
|
|
712
|
+
#
|
|
713
|
+
# @yield the work to run after the response is sent.
|
|
714
|
+
# @return [Boolean] true if a block was registered.
|
|
715
|
+
def after_response(&block)
|
|
716
|
+
return false unless block_given?
|
|
717
|
+
@deferred_callbacks ||= []
|
|
718
|
+
@deferred_callbacks << block
|
|
719
|
+
true
|
|
720
|
+
end
|
|
721
|
+
alias_method :defer, :after_response
|
|
722
|
+
|
|
723
|
+
# @!visibility private
|
|
724
|
+
# The blocks registered via {#after_response}; drained by the Rack app
|
|
725
|
+
# ({Parse::Webhooks.dispatch_deferred}) after the response is finished.
|
|
726
|
+
# @return [Array<Proc>]
|
|
727
|
+
def deferred_callbacks
|
|
728
|
+
@deferred_callbacks ||= []
|
|
729
|
+
end
|
|
730
|
+
|
|
486
731
|
# @return [Parse::Query] the Parse query for a beforeFind trigger.
|
|
487
732
|
def parse_query
|
|
488
733
|
return nil unless parse_class.present? && @query.is_a?(Hash)
|