parse-stack-next 5.0.1 → 5.1.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/.github/ISSUE_TEMPLATE/bug_report.yml +105 -0
- data/.github/ISSUE_TEMPLATE/feature_request.yml +67 -0
- data/.github/dependabot.yml +13 -0
- data/.github/workflows/codeql.yml +1 -1
- data/.github/workflows/docs.yml +3 -3
- data/.github/workflows/release.yml +14 -3
- data/.github/workflows/ruby.yml +1 -1
- data/.gitignore +1 -0
- data/.yardopts +19 -0
- data/CHANGELOG.md +792 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +8 -5
- data/README.md +15 -0
- data/Rakefile +5 -1
- data/docs/acl_clp_guide.md +553 -0
- data/docs/atlas_vector_search_guide.md +123 -22
- data/docs/client_sdk_guide.md +201 -5
- data/docs/usage_guide.md +21 -0
- data/docs/yard-template/default/fulldoc/html/css/common.css +1222 -0
- data/docs/yard-template/default/fulldoc/html/css/full_list.css +387 -0
- data/lib/parse/agent/tools.rb +153 -1
- data/lib/parse/cache/redis.rb +53 -0
- data/lib/parse/client/caching.rb +18 -1
- data/lib/parse/client.rb +79 -12
- data/lib/parse/embeddings/cohere.rb +143 -6
- data/lib/parse/embeddings/provider.rb +20 -2
- data/lib/parse/embeddings/voyage.rb +102 -0
- data/lib/parse/embeddings.rb +332 -1
- data/lib/parse/live_query/client.rb +167 -4
- data/lib/parse/live_query/configuration.rb +12 -0
- data/lib/parse/live_query/subscription.rb +55 -2
- data/lib/parse/live_query.rb +123 -1
- data/lib/parse/lock.rb +342 -0
- data/lib/parse/lock_backend.rb +308 -0
- data/lib/parse/model/classes/audience.rb +5 -0
- data/lib/parse/model/classes/installation.rb +122 -0
- data/lib/parse/model/classes/job_schedule.rb +3 -1
- data/lib/parse/model/classes/job_status.rb +4 -1
- data/lib/parse/model/classes/push_status.rb +4 -1
- data/lib/parse/model/classes/session.rb +7 -0
- data/lib/parse/model/classes/user.rb +204 -0
- data/lib/parse/model/core/create_lock.rb +28 -146
- data/lib/parse/model/core/embed_managed.rb +162 -13
- data/lib/parse/model/core/parse_reference.rb +17 -1
- data/lib/parse/model/core/querying.rb +26 -2
- data/lib/parse/model/file.rb +523 -18
- data/lib/parse/query.rb +31 -1
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +98 -1
- data/parse-stack-next.gemspec +2 -2
- metadata +17 -7
|
@@ -88,18 +88,34 @@ module Parse
|
|
|
88
88
|
# field write; the guard is otherwise closed.
|
|
89
89
|
WRITER_KEY = :parse_embed_managed_writer
|
|
90
90
|
|
|
91
|
-
# Frozen value-object capturing one `embed`
|
|
92
|
-
# the owning class under
|
|
91
|
+
# Frozen value-object capturing one `embed` or `embed_image`
|
|
92
|
+
# declaration. Stored on the owning class under
|
|
93
|
+
# `embed_directives[into]` and passed to
|
|
93
94
|
# {EmbedManaged.recompute_embedding!} from the per-class
|
|
94
95
|
# before_save callback.
|
|
96
|
+
#
|
|
97
|
+
# `modality` is `nil` (treated as `:text`) for {.embed}-declared
|
|
98
|
+
# directives and `:image` for {.embed_image}. The image path
|
|
99
|
+
# routes through `Parse::Embeddings.validate_image_url!` and
|
|
100
|
+
# `Provider#embed_image` rather than `Provider#embed_text`;
|
|
101
|
+
# digest tracking is over the file URL String rather than the
|
|
102
|
+
# concatenated source text.
|
|
103
|
+
#
|
|
104
|
+
# `allow_insecure` is forwarded to {.validate_image_url!} for
|
|
105
|
+
# image directives only; ignored for text.
|
|
95
106
|
EmbedDirective = Struct.new(
|
|
96
107
|
:sources, :into, :digest_field, :input_type, :provider_name,
|
|
108
|
+
:modality, :allow_insecure,
|
|
97
109
|
keyword_init: true,
|
|
98
110
|
) do
|
|
99
111
|
def freeze
|
|
100
112
|
sources.freeze
|
|
101
113
|
super
|
|
102
114
|
end
|
|
115
|
+
|
|
116
|
+
def image?
|
|
117
|
+
modality == :image
|
|
118
|
+
end
|
|
103
119
|
end
|
|
104
120
|
|
|
105
121
|
# @!visibility private
|
|
@@ -186,6 +202,99 @@ module Parse
|
|
|
186
202
|
into
|
|
187
203
|
end
|
|
188
204
|
|
|
205
|
+
# Declare a managed image embedding. Mirrors {.embed} but the
|
|
206
|
+
# source field is a `:file` property (Parse::File) and the
|
|
207
|
+
# provider call routes through {Parse::Embeddings::Provider#embed_image}
|
|
208
|
+
# rather than `#embed_text`. v5.1 ships URL-only: the SDK
|
|
209
|
+
# extracts the file's URL, validates it through
|
|
210
|
+
# {Parse::Embeddings.validate_image_url!} (sentinel-gated egress
|
|
211
|
+
# opt-in, CIDR / port / host allowlist), and forwards the
|
|
212
|
+
# canonicalized URL to the provider. The SDK does NOT download
|
|
213
|
+
# image bytes — bytes-fetch is the v5.3 path.
|
|
214
|
+
#
|
|
215
|
+
# **Digest is the URL string, not the file contents.** Replacing
|
|
216
|
+
# the Parse::File with one pointing to a different URL re-embeds;
|
|
217
|
+
# re-saving the same URL is a no-op (zero provider calls).
|
|
218
|
+
# Cloud-stored Parse files have stable URLs unless overwritten,
|
|
219
|
+
# so this is the right cache key for most uploads. If you mutate
|
|
220
|
+
# the underlying bytes at the SAME URL (e.g. PUT-replace on S3
|
|
221
|
+
# without renaming), the embedding will NOT refresh; rename the
|
|
222
|
+
# file or set `:#{into}_digest` to nil and resave to force re-embed.
|
|
223
|
+
#
|
|
224
|
+
# @param source_field [Symbol] one `:file` property whose URL
|
|
225
|
+
# feeds the provider. (v5.1 accepts a single source per
|
|
226
|
+
# directive; multi-image-per-record support is deferred.)
|
|
227
|
+
# @param into [Symbol] the `:vector` property to populate.
|
|
228
|
+
# Must already be declared with `provider:` metadata.
|
|
229
|
+
# @param input_type [Symbol] forwarded to {Provider#embed_image}.
|
|
230
|
+
# Defaults to `:search_document`.
|
|
231
|
+
# @param digest_field [Symbol, nil] override for the URL-digest
|
|
232
|
+
# sibling. Defaults to `:"#{into}_digest"`. Auto-declared as
|
|
233
|
+
# `:string` if not already declared.
|
|
234
|
+
# @param allow_insecure [Boolean] forwarded to
|
|
235
|
+
# {Parse::Embeddings.validate_image_url!}; permit `http://`
|
|
236
|
+
# for local-dev CDN proxies. Default false.
|
|
237
|
+
# @return [Symbol] the target vector field name.
|
|
238
|
+
# @raise [InvalidEmbedDeclaration] on declaration-time misuse.
|
|
239
|
+
def embed_image(source_field, into:, input_type: :search_document,
|
|
240
|
+
digest_field: nil, allow_insecure: false)
|
|
241
|
+
into = into.to_sym
|
|
242
|
+
unless vector_properties.key?(into)
|
|
243
|
+
raise InvalidEmbedDeclaration,
|
|
244
|
+
"#{self}.embed_image: `into: :#{into}` is not a declared :vector property " \
|
|
245
|
+
"(declared :vector fields: #{vector_properties.keys.inspect})."
|
|
246
|
+
end
|
|
247
|
+
provider_name = vector_properties.dig(into, :provider)
|
|
248
|
+
if provider_name.nil?
|
|
249
|
+
raise InvalidEmbedDeclaration,
|
|
250
|
+
"#{self}.embed_image: `into: :#{into}` has no `provider:` declared on its " \
|
|
251
|
+
":vector property. Add `provider: :voyage` (or another registered name) " \
|
|
252
|
+
"to the property declaration."
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
source = source_field.to_sym
|
|
256
|
+
unless fields.key?(source)
|
|
257
|
+
raise InvalidEmbedDeclaration,
|
|
258
|
+
"#{self}.embed_image: source field #{source.inspect} is not declared on this class."
|
|
259
|
+
end
|
|
260
|
+
unless fields[source] == :file
|
|
261
|
+
raise InvalidEmbedDeclaration,
|
|
262
|
+
"#{self}.embed_image: source field #{source.inspect} must be a :file property " \
|
|
263
|
+
"(got #{fields[source].inspect}). v5.1 image embedding accepts Parse::File " \
|
|
264
|
+
"sources only — text sources go through `embed`."
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
digest_field = (digest_field || :"#{into}_digest").to_sym
|
|
268
|
+
unless fields.key?(digest_field)
|
|
269
|
+
property digest_field, :string
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
directive = EmbedDirective.new(
|
|
273
|
+
sources: [source],
|
|
274
|
+
into: into,
|
|
275
|
+
digest_field: digest_field,
|
|
276
|
+
input_type: input_type,
|
|
277
|
+
provider_name: provider_name,
|
|
278
|
+
modality: :image,
|
|
279
|
+
allow_insecure: allow_insecure,
|
|
280
|
+
).freeze
|
|
281
|
+
embed_directives[into] = directive
|
|
282
|
+
|
|
283
|
+
callback_method = :"_auto_embed_#{into}!"
|
|
284
|
+
define_method(callback_method) do
|
|
285
|
+
Parse::Core::EmbedManaged.recompute_embedding!(self, directive)
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
already_registered = _save_callbacks.any? do |cb|
|
|
289
|
+
cb.kind == :before && (cb.filter.to_sym rescue cb.filter) == callback_method
|
|
290
|
+
end
|
|
291
|
+
before_save callback_method unless already_registered
|
|
292
|
+
|
|
293
|
+
install_embed_writer_guard!(into, [source])
|
|
294
|
+
|
|
295
|
+
into
|
|
296
|
+
end
|
|
297
|
+
|
|
189
298
|
# @!visibility private
|
|
190
299
|
# Prepend a module that intercepts the public `<into>=` setter
|
|
191
300
|
# and raises {ProtectedFieldError} unless the current thread has
|
|
@@ -225,19 +334,19 @@ module Parse
|
|
|
225
334
|
end
|
|
226
335
|
|
|
227
336
|
# @!visibility private
|
|
228
|
-
# before_save body.
|
|
229
|
-
#
|
|
230
|
-
#
|
|
231
|
-
#
|
|
232
|
-
#
|
|
233
|
-
#
|
|
234
|
-
#
|
|
337
|
+
# before_save body. Dispatches on `directive.modality`: text
|
|
338
|
+
# directives concatenate source-field values and call
|
|
339
|
+
# `Provider#embed_text`; image directives extract the source
|
|
340
|
+
# Parse::File's URL, validate it via
|
|
341
|
+
# `Parse::Embeddings.validate_image_url!`, and call
|
|
342
|
+
# `Provider#embed_image`. Digest tracking elides the provider
|
|
343
|
+
# call when the source has not changed since last save.
|
|
235
344
|
def self.recompute_embedding!(record, directive)
|
|
236
|
-
|
|
345
|
+
input = build_source_input(record, directive)
|
|
237
346
|
stored_digest = record.public_send(directive.digest_field)
|
|
238
347
|
target_present = !record.public_send(directive.into).nil?
|
|
239
348
|
|
|
240
|
-
if
|
|
349
|
+
if input.nil? || input.empty?
|
|
241
350
|
if target_present || !stored_digest.nil?
|
|
242
351
|
with_writer(directive.into) do
|
|
243
352
|
record.public_send(:"#{directive.into}=", nil)
|
|
@@ -247,11 +356,11 @@ module Parse
|
|
|
247
356
|
return
|
|
248
357
|
end
|
|
249
358
|
|
|
250
|
-
digest = digest_for(
|
|
359
|
+
digest = digest_for(input)
|
|
251
360
|
return if stored_digest == digest && target_present
|
|
252
361
|
|
|
253
362
|
provider = Parse::Embeddings.provider(directive.provider_name)
|
|
254
|
-
vectors = provider
|
|
363
|
+
vectors = call_provider(provider, directive, input)
|
|
255
364
|
unless vectors.is_a?(Array) && vectors.length == 1 && vectors.first.is_a?(Array)
|
|
256
365
|
raise Parse::Embeddings::InvalidResponseError,
|
|
257
366
|
"Parse::Core::EmbedManaged (#{record.class}##{directive.into}): provider " \
|
|
@@ -273,6 +382,46 @@ module Parse
|
|
|
273
382
|
record.public_send(:"#{directive.digest_field}=", digest)
|
|
274
383
|
end
|
|
275
384
|
|
|
385
|
+
# @!visibility private
|
|
386
|
+
# Build the provider input for `directive`: concatenated text for
|
|
387
|
+
# text directives; the raw image URL for image directives.
|
|
388
|
+
# Returns `nil` (treated as "clear the embedding") when the source
|
|
389
|
+
# is absent, empty, or — for images — has no URL.
|
|
390
|
+
#
|
|
391
|
+
# **Image path does not validate here.** Validation runs once,
|
|
392
|
+
# inside the provider's `embed_image` call. Validating here
|
|
393
|
+
# would double-resolve every URL (round-2 audit LOW #3) since
|
|
394
|
+
# provider implementations call `validate_image_url!` again.
|
|
395
|
+
# The digest is computed from the raw URL string, which is fine
|
|
396
|
+
# — the digest is a content fingerprint, not a security boundary.
|
|
397
|
+
# If validation fails, the provider raises `InvalidImageURL` /
|
|
398
|
+
# `ConfirmationRequired` from inside `recompute_embedding!`, which
|
|
399
|
+
# surfaces from `before_save` exactly as before.
|
|
400
|
+
def self.build_source_input(record, directive)
|
|
401
|
+
if directive.image?
|
|
402
|
+
source_field = directive.sources.first
|
|
403
|
+
file = record.public_send(source_field)
|
|
404
|
+
return nil if file.nil?
|
|
405
|
+
url = file.respond_to?(:url) ? file.url : nil
|
|
406
|
+
return nil if url.nil? || url.to_s.empty?
|
|
407
|
+
url.to_s
|
|
408
|
+
else
|
|
409
|
+
build_source_text(record, directive.sources)
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
# @!visibility private
|
|
414
|
+
# Dispatch the provider call based on directive modality.
|
|
415
|
+
def self.call_provider(provider, directive, input)
|
|
416
|
+
if directive.image?
|
|
417
|
+
provider.embed_image([input],
|
|
418
|
+
input_type: directive.input_type,
|
|
419
|
+
allow_insecure: directive.allow_insecure ? true : false)
|
|
420
|
+
else
|
|
421
|
+
provider.embed_text([input], input_type: directive.input_type)
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
|
|
276
425
|
# @!visibility private
|
|
277
426
|
# Concatenate source-field string values. `nil` and blank entries
|
|
278
427
|
# are skipped; remaining values are joined with a double newline.
|
|
@@ -224,7 +224,23 @@ module Parse
|
|
|
224
224
|
if respond_to?(:protect_fields) && respond_to?(:class_permissions)
|
|
225
225
|
existing = class_permissions.protected_fields_for("*") rescue []
|
|
226
226
|
merged = (existing + [field_name.to_s]).uniq
|
|
227
|
-
protect_fields
|
|
227
|
+
# Suppress Parse::User's "raw protect_fields called" advisory
|
|
228
|
+
# for this internal auto-install. The advisory exists to nudge
|
|
229
|
+
# app code toward the new master_only_fields/self_visible_fields
|
|
230
|
+
# DSL; the parse_reference auto-install is a different concern
|
|
231
|
+
# and should not trip it at gem boot.
|
|
232
|
+
prior = nil
|
|
233
|
+
if is_a?(Class) && self <= Parse::User
|
|
234
|
+
prior = instance_variable_get(:@_user_field_dsl_active)
|
|
235
|
+
instance_variable_set(:@_user_field_dsl_active, true)
|
|
236
|
+
end
|
|
237
|
+
begin
|
|
238
|
+
protect_fields("*", merged)
|
|
239
|
+
ensure
|
|
240
|
+
if is_a?(Class) && self <= Parse::User
|
|
241
|
+
instance_variable_set(:@_user_field_dsl_active, prior)
|
|
242
|
+
end
|
|
243
|
+
end
|
|
228
244
|
end
|
|
229
245
|
|
|
230
246
|
# Auto-install write-side protection: once the after_create
|
|
@@ -435,10 +435,28 @@ module Parse
|
|
|
435
435
|
# @param fields [Array<String>] specific fields to watch for changes (nil = all fields)
|
|
436
436
|
# @param session_token [String] session token for ACL-aware subscriptions
|
|
437
437
|
# @param client [Parse::LiveQuery::Client] custom LiveQuery client (optional)
|
|
438
|
+
# @param use_master_key [Boolean] per-subscription master-key opt-in.
|
|
439
|
+
# See {Parse::Query#subscribe} for the full description.
|
|
440
|
+
# @yield [subscription] runs the block with the freshly-constructed
|
|
441
|
+
# {Parse::LiveQuery::Subscription} BEFORE the subscribe frame is
|
|
442
|
+
# sent so caller-registered callbacks are wired before any server
|
|
443
|
+
# events can arrive. Optional — callers may still capture the
|
|
444
|
+
# returned subscription and register callbacks later.
|
|
445
|
+
# @example block form (ergonomic, no race window)
|
|
446
|
+
# Post.subscribe(where: { published: true }) do |sub|
|
|
447
|
+
# sub.on(:create) { |obj| puts "new: #{obj.id}" }
|
|
448
|
+
# sub.on(:update) { |obj, prev| puts "updated: #{obj.id}" }
|
|
449
|
+
# end
|
|
450
|
+
# @example capture-then-wire form (equivalent, but has a tiny
|
|
451
|
+
# window between subscribe-frame send and the first .on call
|
|
452
|
+
# where a server event would be dropped if it arrived first)
|
|
453
|
+
# sub = Post.subscribe(where: { published: true })
|
|
454
|
+
# sub.on(:create) { |obj| … }
|
|
438
455
|
# @return [Parse::LiveQuery::Subscription] the subscription object
|
|
439
456
|
# @see Parse::LiveQuery::Subscription
|
|
440
457
|
# @see Parse::Query#subscribe
|
|
441
|
-
def subscribe(where: {}, fields: nil, session_token: nil, client: nil
|
|
458
|
+
def subscribe(where: {}, fields: nil, session_token: nil, client: nil,
|
|
459
|
+
use_master_key: false, &block)
|
|
442
460
|
# Fall through to the ambient set by `Parse.with_session` / `Parse.login`
|
|
443
461
|
# so a caller wrapping a region with `with_session(user) { Klass.subscribe ... }`
|
|
444
462
|
# gets an ACL-aware subscription without re-threading the token.
|
|
@@ -446,7 +464,13 @@ module Parse
|
|
|
446
464
|
ambient = Parse.current_session_token
|
|
447
465
|
session_token = ambient if ambient.is_a?(String) && !ambient.empty?
|
|
448
466
|
end
|
|
449
|
-
query(where).subscribe(
|
|
467
|
+
query(where).subscribe(
|
|
468
|
+
fields: fields,
|
|
469
|
+
session_token: session_token,
|
|
470
|
+
client: client,
|
|
471
|
+
use_master_key: use_master_key,
|
|
472
|
+
&block
|
|
473
|
+
)
|
|
450
474
|
end
|
|
451
475
|
|
|
452
476
|
# Find objects for a given objectId in this collection. The result is a list
|