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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +634 -0
- data/README.md +378 -6
- data/docs/caching.md +748 -0
- data/examples/basic_client.rb +3 -3
- data/examples/basic_server.rb +3 -3
- data/examples/live_query_listener.rb +12 -12
- data/examples/rag_chatbot.rb +14 -14
- data/examples/transaction_example.rb +44 -45
- data/examples/webhook_server.rb +3 -3
- data/lib/parse/access.rb +493 -0
- data/lib/parse/acl_scope.rb +125 -52
- data/lib/parse/agent/approval_gate.rb +0 -0
- data/lib/parse/agent/cancellation_token.rb +3 -3
- data/lib/parse/agent/constraint_translator.rb +4 -4
- data/lib/parse/agent/describe.rb +34 -34
- data/lib/parse/agent/errors.rb +9 -9
- data/lib/parse/agent/mcp_client.rb +61 -59
- data/lib/parse/agent/mcp_dispatcher.rb +89 -101
- data/lib/parse/agent/mcp_rack_app.rb +92 -93
- data/lib/parse/agent/mcp_server.rb +1 -1
- data/lib/parse/agent/mcp_subscriptions.rb +22 -22
- data/lib/parse/agent/metadata_audit.rb +1 -2
- data/lib/parse/agent/metadata_dsl.rb +17 -7
- data/lib/parse/agent/metadata_registry.rb +12 -12
- data/lib/parse/agent/prompt_hardening.rb +4 -4
- data/lib/parse/agent/prompts.rb +10 -10
- data/lib/parse/agent/result_formatter.rb +4 -4
- data/lib/parse/agent/tools.rb +493 -400
- data/lib/parse/agent.rb +157 -116
- data/lib/parse/api/hooks.rb +1 -1
- data/lib/parse/api/server.rb +2 -2
- data/lib/parse/api/users.rb +2 -2
- data/lib/parse/atlas_search/index_manager.rb +1 -1
- data/lib/parse/atlas_search/session.rb +40 -218
- data/lib/parse/atlas_search.rb +181 -60
- data/lib/parse/authorization.rb +466 -0
- data/lib/parse/cache/invalidation.rb +167 -0
- data/lib/parse/cache/keyspace.rb +306 -0
- data/lib/parse/cache/moneta_surface.rb +126 -0
- data/lib/parse/cache/pool.rb +48 -5
- data/lib/parse/cache/redis.rb +425 -10
- data/lib/parse/cache/scoped_view.rb +443 -0
- data/lib/parse/cache/sub_cache.rb +264 -0
- data/lib/parse/cache/upstream_roles.rb +230 -0
- data/lib/parse/client/authentication.rb +1 -1
- data/lib/parse/client/body_builder.rb +1 -2
- data/lib/parse/client/caching.rb +84 -5
- data/lib/parse/client.rb +327 -189
- data/lib/parse/clp_scope.rb +225 -28
- data/lib/parse/console.rb +3 -3
- data/lib/parse/embeddings/batch_embedder.rb +3 -3
- data/lib/parse/embeddings/binding_audit.rb +211 -0
- data/lib/parse/embeddings/cache.rb +17 -17
- data/lib/parse/embeddings/cohere.rb +21 -22
- data/lib/parse/embeddings/image_fetch.rb +22 -21
- data/lib/parse/embeddings/jina.rb +21 -21
- data/lib/parse/embeddings/local_http.rb +6 -7
- data/lib/parse/embeddings/media_file.rb +137 -0
- data/lib/parse/embeddings/openai.rb +5 -5
- data/lib/parse/embeddings/provider.rb +40 -1
- data/lib/parse/embeddings/qwen.rb +10 -10
- data/lib/parse/embeddings/spend_cap.rb +2 -3
- data/lib/parse/embeddings/streaming_body.rb +170 -0
- data/lib/parse/embeddings/video_source.rb +121 -0
- data/lib/parse/embeddings/voyage.rb +554 -143
- data/lib/parse/embeddings.rb +83 -27
- data/lib/parse/graphql/type_generator.rb +1 -1
- data/lib/parse/graphql.rb +5 -5
- data/lib/parse/live_query/client.rb +1 -1
- data/lib/parse/live_query.rb +1 -1
- data/lib/parse/lock.rb +15 -18
- data/lib/parse/lock_backend.rb +1 -1
- data/lib/parse/lookup_rewriter.rb +6 -6
- data/lib/parse/model/classes/role.rb +227 -28
- data/lib/parse/model/classes/user.rb +60 -1
- data/lib/parse/model/clp.rb +4 -4
- data/lib/parse/model/core/actions.rb +97 -15
- data/lib/parse/model/core/create_lock.rb +0 -2
- data/lib/parse/model/core/describe.rb +57 -57
- data/lib/parse/model/core/embed_managed.rb +43 -19
- data/lib/parse/model/core/fetching.rb +2 -0
- data/lib/parse/model/core/indexing.rb +14 -14
- data/lib/parse/model/core/parse_reference.rb +1 -1
- data/lib/parse/model/core/properties.rb +39 -3
- data/lib/parse/model/core/querying.rb +1 -1
- data/lib/parse/model/core/schema.rb +2 -2
- data/lib/parse/model/core/search_indexing.rb +2 -2
- data/lib/parse/model/core/vector_searchable.rb +34 -8
- data/lib/parse/model/file.rb +21 -15
- data/lib/parse/model/geojson.rb +2 -2
- data/lib/parse/model/geopoint.rb +1 -0
- data/lib/parse/model/object.rb +132 -66
- data/lib/parse/model/pointer.rb +2 -0
- data/lib/parse/model/polygon.rb +3 -6
- data/lib/parse/model/push.rb +2 -2
- data/lib/parse/model/vector.rb +3 -1
- data/lib/parse/mongodb.rb +512 -282
- data/lib/parse/pipeline_security.rb +3 -0
- data/lib/parse/query/constraints.rb +46 -46
- data/lib/parse/query.rb +146 -59
- data/lib/parse/retrieval/agent_tool.rb +34 -20
- data/lib/parse/retrieval/chunk.rb +1 -0
- data/lib/parse/retrieval/reranker/cohere.rb +11 -11
- data/lib/parse/retrieval/reranker.rb +3 -4
- data/lib/parse/retrieval/retriever.rb +4 -6
- data/lib/parse/schema/index_migrator.rb +26 -26
- data/lib/parse/schema/search_index_migrator.rb +19 -19
- data/lib/parse/stack/tasks.rb +6 -6
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +10 -10
- data/lib/parse/vector_search/hybrid.rb +166 -33
- data/lib/parse/vector_search.rb +141 -13
- data/lib/parse/webhooks/payload.rb +1 -0
- data/lib/parse/webhooks/registration.rb +4 -4
- data/lib/parse/webhooks/trigger_audit.rb +50 -49
- data/lib/parse/webhooks.rb +22 -4
- data/parse-stack-next.gemspec +6 -6
- metadata +14 -1
|
@@ -18,9 +18,9 @@ module Parse
|
|
|
18
18
|
# gracefully (`{available: false, reason: ...}`) instead of raising
|
|
19
19
|
# when the underlying service is unreachable or unconfigured.
|
|
20
20
|
module Describe
|
|
21
|
-
LOCAL_SECTIONS
|
|
21
|
+
LOCAL_SECTIONS = %i[model acl].freeze
|
|
22
22
|
NETWORK_SECTIONS = %i[schema clp atlas indexes].freeze
|
|
23
|
-
ALL_SECTIONS
|
|
23
|
+
ALL_SECTIONS = (LOCAL_SECTIONS + NETWORK_SECTIONS).freeze
|
|
24
24
|
|
|
25
25
|
# Core/built-in field keys we don't report under `:model[:fields]` —
|
|
26
26
|
# they're inherited from Parse::Object (in both snake_case and
|
|
@@ -55,7 +55,7 @@ module Parse
|
|
|
55
55
|
# @return [Hash, String]
|
|
56
56
|
def describe(*sections, pretty: false, network: false, usage: false, master: false, client: nil)
|
|
57
57
|
requested = sections.flatten.map(&:to_sym)
|
|
58
|
-
active
|
|
58
|
+
active = if requested.empty?
|
|
59
59
|
network ? ALL_SECTIONS : LOCAL_SECTIONS
|
|
60
60
|
else
|
|
61
61
|
requested
|
|
@@ -72,13 +72,13 @@ module Parse
|
|
|
72
72
|
|
|
73
73
|
def describe_section(section, client:, network:, usage: false, master: false)
|
|
74
74
|
case section
|
|
75
|
-
when :model
|
|
76
|
-
when :acl
|
|
75
|
+
when :model then describe_model_section
|
|
76
|
+
when :acl then describe_acl_section
|
|
77
77
|
when :schema then network_section(network) { describe_schema_section(client) }
|
|
78
|
-
when :clp
|
|
79
|
-
when :atlas
|
|
78
|
+
when :clp then network_section(network) { describe_clp_section(client) }
|
|
79
|
+
when :atlas then network_section(network) { describe_atlas_section }
|
|
80
80
|
when :indexes then network_section(network) { describe_indexes_section(usage: usage, master: master) }
|
|
81
|
-
else
|
|
81
|
+
else { available: false, reason: :unknown_section }
|
|
82
82
|
end
|
|
83
83
|
end
|
|
84
84
|
|
|
@@ -92,15 +92,15 @@ module Parse
|
|
|
92
92
|
def describe_model_section
|
|
93
93
|
local_fields = fields.reject { |k, _| CORE_FIELD_KEYS.include?(k) }
|
|
94
94
|
{
|
|
95
|
-
parse_class:
|
|
96
|
-
fields:
|
|
97
|
-
field_count:
|
|
98
|
-
references:
|
|
99
|
-
relations:
|
|
100
|
-
defaults:
|
|
101
|
-
enums:
|
|
102
|
-
agent_fields:
|
|
103
|
-
|
|
95
|
+
parse_class: parse_class,
|
|
96
|
+
fields: local_fields,
|
|
97
|
+
field_count: local_fields.size,
|
|
98
|
+
references: (respond_to?(:references) ? references.dup : {}),
|
|
99
|
+
relations: (respond_to?(:relations) ? relations.dup : {}),
|
|
100
|
+
defaults: (respond_to?(:defaults_list) ? defaults_list.dup : []),
|
|
101
|
+
enums: (respond_to?(:enums) ? enums.dup : {}),
|
|
102
|
+
agent_fields: (respond_to?(:agent_field_allowlist) && agent_field_allowlist.any? ?
|
|
103
|
+
agent_field_allowlist.map(&:to_s).sort : nil),
|
|
104
104
|
agent_methods: agent_method_names_or_nil,
|
|
105
105
|
}
|
|
106
106
|
end
|
|
@@ -115,9 +115,9 @@ module Parse
|
|
|
115
115
|
def describe_acl_section
|
|
116
116
|
defaults = respond_to?(:default_acls) ? default_acls : nil
|
|
117
117
|
{
|
|
118
|
-
default_acl:
|
|
118
|
+
default_acl: defaults.respond_to?(:as_json) ? defaults.as_json : nil,
|
|
119
119
|
default_acl_private: (respond_to?(:default_acl_private) ? !!default_acl_private : nil),
|
|
120
|
-
acl_policy:
|
|
120
|
+
acl_policy: instance_variable_get(:@acl_policy_setting),
|
|
121
121
|
}
|
|
122
122
|
end
|
|
123
123
|
|
|
@@ -126,12 +126,12 @@ module Parse
|
|
|
126
126
|
return { available: false, reason: :class_missing_on_server } if info.nil?
|
|
127
127
|
diff = Parse::Schema::SchemaDiff.new(self, info)
|
|
128
128
|
{
|
|
129
|
-
available:
|
|
130
|
-
in_sync:
|
|
129
|
+
available: true,
|
|
130
|
+
in_sync: diff.in_sync?,
|
|
131
131
|
server_field_count: info.field_names.size,
|
|
132
|
-
missing_on_server:
|
|
133
|
-
missing_locally:
|
|
134
|
-
type_mismatches:
|
|
132
|
+
missing_on_server: diff.missing_on_server,
|
|
133
|
+
missing_locally: diff.missing_locally,
|
|
134
|
+
type_mismatches: diff.type_mismatches,
|
|
135
135
|
}
|
|
136
136
|
end
|
|
137
137
|
|
|
@@ -148,10 +148,10 @@ module Parse
|
|
|
148
148
|
indexes = Parse::AtlasSearch::IndexManager.list_indexes(parse_class)
|
|
149
149
|
{
|
|
150
150
|
available: true,
|
|
151
|
-
count:
|
|
152
|
-
indexes:
|
|
153
|
-
{ name:
|
|
154
|
-
status:
|
|
151
|
+
count: indexes.size,
|
|
152
|
+
indexes: indexes.map { |i|
|
|
153
|
+
{ name: i["name"],
|
|
154
|
+
status: i["status"],
|
|
155
155
|
queryable: i["queryable"] }
|
|
156
156
|
},
|
|
157
157
|
}
|
|
@@ -183,8 +183,8 @@ module Parse
|
|
|
183
183
|
normalized = raw.map { |idx| normalize_index_entry(idx, stats: stats) }
|
|
184
184
|
result = {
|
|
185
185
|
available: true,
|
|
186
|
-
count:
|
|
187
|
-
indexes:
|
|
186
|
+
count: normalized.size,
|
|
187
|
+
indexes: normalized,
|
|
188
188
|
}
|
|
189
189
|
if usage
|
|
190
190
|
# Empty stats Hash means the role lacks clusterMonitor / Atlas
|
|
@@ -203,22 +203,22 @@ module Parse
|
|
|
203
203
|
# plans for any `_Join:*` collections from
|
|
204
204
|
# `mongo_relation_index`.
|
|
205
205
|
plans = Parse::Schema::IndexMigrator.new(self).plan
|
|
206
|
-
base
|
|
206
|
+
base = parse_class
|
|
207
207
|
parent_plan = plans[base]
|
|
208
208
|
if parent_plan
|
|
209
|
-
result[:declared]
|
|
210
|
-
result[:drift]
|
|
209
|
+
result[:declared] = parent_plan[:declared].map { |d| describe_decl(d) }
|
|
210
|
+
result[:drift] = describe_drift(parent_plan)
|
|
211
211
|
result[:parse_managed] = parent_plan[:parse_managed]
|
|
212
|
-
result[:capacity]
|
|
212
|
+
result[:capacity] = describe_capacity(parent_plan)
|
|
213
213
|
end
|
|
214
214
|
join_plans = plans.reject { |k, _| k == base }
|
|
215
215
|
unless join_plans.empty?
|
|
216
216
|
result[:relations] = join_plans.each_with_object({}) do |(coll, p), h|
|
|
217
217
|
h[coll] = {
|
|
218
|
-
declared:
|
|
219
|
-
drift:
|
|
218
|
+
declared: p[:declared].map { |d| describe_decl(d) },
|
|
219
|
+
drift: describe_drift(p),
|
|
220
220
|
parse_managed: p[:parse_managed],
|
|
221
|
-
capacity:
|
|
221
|
+
capacity: describe_capacity(p),
|
|
222
222
|
}
|
|
223
223
|
end
|
|
224
224
|
end
|
|
@@ -234,18 +234,18 @@ module Parse
|
|
|
234
234
|
def describe_drift(plan)
|
|
235
235
|
{
|
|
236
236
|
to_create: plan[:to_create].map { |d| describe_decl(d) },
|
|
237
|
-
in_sync:
|
|
238
|
-
orphans:
|
|
237
|
+
in_sync: plan[:in_sync].map { |d| describe_decl(d) },
|
|
238
|
+
orphans: plan[:orphans],
|
|
239
239
|
conflicts: plan[:conflicts],
|
|
240
240
|
}
|
|
241
241
|
end
|
|
242
242
|
|
|
243
243
|
def describe_capacity(plan)
|
|
244
244
|
{
|
|
245
|
-
used:
|
|
246
|
-
after:
|
|
245
|
+
used: plan[:capacity_used],
|
|
246
|
+
after: plan[:capacity_after],
|
|
247
247
|
remaining: plan[:capacity_remaining],
|
|
248
|
-
ok:
|
|
248
|
+
ok: plan[:capacity_ok],
|
|
249
249
|
}
|
|
250
250
|
end
|
|
251
251
|
|
|
@@ -259,11 +259,11 @@ module Parse
|
|
|
259
259
|
def normalize_index_entry(idx, stats: {})
|
|
260
260
|
name = idx["name"] || idx[:name]
|
|
261
261
|
entry = {
|
|
262
|
-
name:
|
|
263
|
-
implicit_id:
|
|
264
|
-
key:
|
|
265
|
-
unique:
|
|
266
|
-
sparse:
|
|
262
|
+
name: name,
|
|
263
|
+
implicit_id: name == "_id_",
|
|
264
|
+
key: coerce_bson(idx["key"] || idx[:key] || {}),
|
|
265
|
+
unique: idx["unique"] == true,
|
|
266
|
+
sparse: idx["sparse"] == true,
|
|
267
267
|
partial_filter: coerce_bson(idx["partialFilterExpression"] || idx[:partialFilterExpression]),
|
|
268
268
|
expire_after_seconds: idx["expireAfterSeconds"] || idx[:expireAfterSeconds],
|
|
269
269
|
}
|
|
@@ -292,10 +292,10 @@ module Parse
|
|
|
292
292
|
if (m = data[:model])
|
|
293
293
|
lines << " fields: #{m[:field_count]}"
|
|
294
294
|
lines << " references: #{m[:references].inspect}" if m[:references].any?
|
|
295
|
-
lines << " relations: #{m[:relations].inspect}"
|
|
296
|
-
lines << " defaults: #{m[:defaults].inspect}"
|
|
295
|
+
lines << " relations: #{m[:relations].inspect}" if m[:relations].any?
|
|
296
|
+
lines << " defaults: #{m[:defaults].inspect}" if m[:defaults].any?
|
|
297
297
|
lines << " enums: #{m[:enums].keys.inspect}" if m[:enums].any?
|
|
298
|
-
lines << " agent_fields: #{m[:agent_fields].inspect}"
|
|
298
|
+
lines << " agent_fields: #{m[:agent_fields].inspect}" if m[:agent_fields]
|
|
299
299
|
lines << " agent_methods: #{m[:agent_methods].inspect}" if m[:agent_methods]
|
|
300
300
|
end
|
|
301
301
|
|
|
@@ -309,8 +309,8 @@ module Parse
|
|
|
309
309
|
label = s[:in_sync] ? "in sync" : "drifted"
|
|
310
310
|
lines << " schema: #{label} (server fields=#{s[:server_field_count]})"
|
|
311
311
|
lines << " missing_on_server: #{s[:missing_on_server].keys.inspect}" if s[:missing_on_server].any?
|
|
312
|
-
lines << " missing_locally: #{s[:missing_locally].keys.inspect}"
|
|
313
|
-
lines << " type_mismatches: #{s[:type_mismatches].keys.inspect}"
|
|
312
|
+
lines << " missing_locally: #{s[:missing_locally].keys.inspect}" if s[:missing_locally].any?
|
|
313
|
+
lines << " type_mismatches: #{s[:type_mismatches].keys.inspect}" if s[:type_mismatches].any?
|
|
314
314
|
else
|
|
315
315
|
lines << " schema: unavailable (#{s[:reason]})"
|
|
316
316
|
end
|
|
@@ -351,10 +351,10 @@ module Parse
|
|
|
351
351
|
end
|
|
352
352
|
if (drift = ix[:drift])
|
|
353
353
|
lines << " declared: #{ix[:declared].size}"
|
|
354
|
-
lines << " to_create: #{drift[:to_create].size}"
|
|
355
|
-
lines << " in_sync: #{drift[:in_sync].size}"
|
|
356
|
-
lines << " orphans: #{drift[:orphans].inspect}"
|
|
357
|
-
lines << " conflicts: #{drift[:conflicts].size}"
|
|
354
|
+
lines << " to_create: #{drift[:to_create].size}" if drift[:to_create].any?
|
|
355
|
+
lines << " in_sync: #{drift[:in_sync].size}" if drift[:in_sync].any?
|
|
356
|
+
lines << " orphans: #{drift[:orphans].inspect}" if drift[:orphans].any?
|
|
357
|
+
lines << " conflicts: #{drift[:conflicts].size}" if drift[:conflicts].any?
|
|
358
358
|
end
|
|
359
359
|
if (cap = ix[:capacity])
|
|
360
360
|
lines << " capacity: #{cap[:used]}/#{Parse::Core::Indexing::MAX_INDEXES_PER_COLLECTION} (#{cap[:remaining]} remaining)"
|
|
@@ -366,7 +366,7 @@ module Parse
|
|
|
366
366
|
lines << " declared: #{info[:declared].size}"
|
|
367
367
|
d = info[:drift]
|
|
368
368
|
lines << " to_create: #{d[:to_create].size}" if d[:to_create].any?
|
|
369
|
-
lines << " in_sync: #{d[:in_sync].size}"
|
|
369
|
+
lines << " in_sync: #{d[:in_sync].size}" if d[:in_sync].any?
|
|
370
370
|
lines << " orphans: #{d[:orphans].inspect}" if d[:orphans].any?
|
|
371
371
|
end
|
|
372
372
|
end
|
|
@@ -162,8 +162,7 @@ module Parse
|
|
|
162
162
|
if directives.empty?
|
|
163
163
|
raise ArgumentError, "#{self.class}#compute_embedding!: no `embed` directives declared."
|
|
164
164
|
end
|
|
165
|
-
selected =
|
|
166
|
-
if field
|
|
165
|
+
selected = if field
|
|
167
166
|
d = directives[field.to_sym]
|
|
168
167
|
unless d
|
|
169
168
|
raise ArgumentError,
|
|
@@ -211,7 +210,7 @@ module Parse
|
|
|
211
210
|
# @return [Symbol] the target vector field name.
|
|
212
211
|
# @raise [InvalidEmbedDeclaration] on declaration-time misuse.
|
|
213
212
|
def embed(*source_fields, into:, input_type: :search_document, digest_field: nil,
|
|
214
|
-
|
|
213
|
+
meta_field: nil)
|
|
215
214
|
if source_fields.empty?
|
|
216
215
|
raise InvalidEmbedDeclaration,
|
|
217
216
|
"#{self}.embed: at least one source field is required."
|
|
@@ -325,8 +324,8 @@ module Parse
|
|
|
325
324
|
# @return [Symbol] the target vector field name.
|
|
326
325
|
# @raise [InvalidEmbedDeclaration] on declaration-time misuse.
|
|
327
326
|
def embed_image(source_field, into:, input_type: :search_document,
|
|
328
|
-
|
|
329
|
-
|
|
327
|
+
digest_field: nil, allow_insecure: false,
|
|
328
|
+
source: :url, exif_strip: true, meta_field: nil)
|
|
330
329
|
# Capture the fetch mode immediately — the legacy local
|
|
331
330
|
# `source = source_field.to_sym` below shadows the kwarg.
|
|
332
331
|
source_mode = source_mode_for_embed_image!(source)
|
|
@@ -489,12 +488,12 @@ module Parse
|
|
|
489
488
|
# The provenance tuple a freshly-embedded row would carry today.
|
|
490
489
|
def current_embed_identity(directive)
|
|
491
490
|
model = begin
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
491
|
+
Parse::Embeddings.provider(directive.provider_name).model_name
|
|
492
|
+
rescue Parse::Embeddings::ProviderNotRegistered
|
|
493
|
+
raise
|
|
494
|
+
rescue NotImplementedError
|
|
495
|
+
nil
|
|
496
|
+
end
|
|
498
497
|
{
|
|
499
498
|
"provider" => directive.provider_name.to_s,
|
|
500
499
|
"model" => model,
|
|
@@ -642,6 +641,20 @@ module Parse
|
|
|
642
641
|
# `Parse::Embeddings.validate_image_url!`, and call
|
|
643
642
|
# `Provider#embed_image`. Digest tracking elides the provider
|
|
644
643
|
# call when the source has not changed since last save.
|
|
644
|
+
# @!visibility private
|
|
645
|
+
# Run the shared binding validator when the declared provider is
|
|
646
|
+
# actually registered. Delegates to
|
|
647
|
+
# {Parse::Embeddings::BindingAudit} so the lazy path and the
|
|
648
|
+
# explicit `audit_all!` path apply identical rules.
|
|
649
|
+
def self.audit_binding!(klass, directive)
|
|
650
|
+
provider = begin
|
|
651
|
+
Parse::Embeddings.provider(directive.provider_name)
|
|
652
|
+
rescue Parse::Embeddings::ProviderNotRegistered
|
|
653
|
+
return
|
|
654
|
+
end
|
|
655
|
+
Parse::Embeddings::BindingAudit.verify!(klass, directive.into, provider)
|
|
656
|
+
end
|
|
657
|
+
|
|
645
658
|
def self.recompute_embedding!(record, directive)
|
|
646
659
|
input = build_source_input(record, directive)
|
|
647
660
|
stored_digest = record.public_send(directive.digest_field)
|
|
@@ -658,6 +671,18 @@ module Parse
|
|
|
658
671
|
return
|
|
659
672
|
end
|
|
660
673
|
|
|
674
|
+
# Audit the binding BEFORE the digest early-return, not after.
|
|
675
|
+
# A drifted `model:` is a configuration fault, not a property of
|
|
676
|
+
# this particular record: if the check sat behind the digest
|
|
677
|
+
# gate, an unchanged record would skip it and the mismatch would
|
|
678
|
+
# stay invisible until some unrelated record happened to be
|
|
679
|
+
# edited. Saving any record with this directive should surface
|
|
680
|
+
# it. Skipped silently when the provider is not registered — the
|
|
681
|
+
# resolution below still raises for records that actually embed,
|
|
682
|
+
# so an unchanged save keeps its existing no-provider-needed
|
|
683
|
+
# behavior.
|
|
684
|
+
audit_binding!(record.class, directive)
|
|
685
|
+
|
|
661
686
|
digest = digest_for(input)
|
|
662
687
|
return if stored_digest == digest && target_present
|
|
663
688
|
|
|
@@ -694,10 +719,10 @@ module Parse
|
|
|
694
719
|
return if directive.meta_field.nil?
|
|
695
720
|
return unless record.respond_to?(:"#{directive.meta_field}=")
|
|
696
721
|
model = begin
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
722
|
+
provider.model_name
|
|
723
|
+
rescue NotImplementedError
|
|
724
|
+
nil
|
|
725
|
+
end
|
|
701
726
|
record.public_send(:"#{directive.meta_field}=", {
|
|
702
727
|
"provider" => directive.provider_name.to_s,
|
|
703
728
|
"model" => model,
|
|
@@ -751,8 +776,7 @@ module Parse
|
|
|
751
776
|
# fetches it itself).
|
|
752
777
|
def self.call_provider(provider, directive, input)
|
|
753
778
|
if directive.image?
|
|
754
|
-
source =
|
|
755
|
-
if directive.bytes_mode?
|
|
779
|
+
source = if directive.bytes_mode?
|
|
756
780
|
Parse::Embeddings::ImageFetch.fetch!(
|
|
757
781
|
input,
|
|
758
782
|
allow_insecure: directive.allow_insecure ? true : false,
|
|
@@ -762,8 +786,8 @@ module Parse
|
|
|
762
786
|
input
|
|
763
787
|
end
|
|
764
788
|
provider.embed_image([source],
|
|
765
|
-
|
|
766
|
-
|
|
789
|
+
input_type: directive.input_type,
|
|
790
|
+
allow_insecure: directive.allow_insecure ? true : false)
|
|
767
791
|
else
|
|
768
792
|
provider.embed_text([input], input_type: directive.input_type)
|
|
769
793
|
end
|
|
@@ -203,6 +203,8 @@ module Parse
|
|
|
203
203
|
|
|
204
204
|
# Apply attributes from server (only keys in result get updated)
|
|
205
205
|
apply_attributes!(result, dirty_track: false)
|
|
206
|
+
record_authorization_hydration!(result, partial: is_partial_fetch) if
|
|
207
|
+
respond_to?(:record_authorization_hydration!)
|
|
206
208
|
|
|
207
209
|
begin
|
|
208
210
|
clear_changes!
|
|
@@ -93,9 +93,9 @@ module Parse
|
|
|
93
93
|
# @raise [ArgumentError] when validation rules fail (no fields,
|
|
94
94
|
# unknown field, parallel arrays, relation field, etc.)
|
|
95
95
|
def mongo_index(*fields, unique: false, sparse: false, partial: nil,
|
|
96
|
-
|
|
96
|
+
expire_after: nil, name: nil)
|
|
97
97
|
register_index(fields, key_value: 1, unique: unique, sparse: sparse,
|
|
98
|
-
|
|
98
|
+
partial: partial, expire_after: expire_after, name: name)
|
|
99
99
|
end
|
|
100
100
|
|
|
101
101
|
# Declare a UNIQUE index on the exact dedup tuple that
|
|
@@ -175,7 +175,7 @@ module Parse
|
|
|
175
175
|
# which `2dsphere` indexes natively.
|
|
176
176
|
def mongo_geo_index(field, sparse: false, name: nil)
|
|
177
177
|
register_index([field], key_value: "2dsphere", unique: false,
|
|
178
|
-
|
|
178
|
+
sparse: sparse, partial: nil, expire_after: nil, name: name)
|
|
179
179
|
end
|
|
180
180
|
|
|
181
181
|
# Declare an index on a Parse Relation's join collection. Relations
|
|
@@ -277,7 +277,7 @@ module Parse
|
|
|
277
277
|
private
|
|
278
278
|
|
|
279
279
|
def register_index(fields, key_value:, unique:, sparse:, partial:,
|
|
280
|
-
|
|
280
|
+
expire_after:, name:)
|
|
281
281
|
fields = fields.flatten.map(&:to_sym)
|
|
282
282
|
if fields.empty?
|
|
283
283
|
raise ArgumentError, "#{self}.mongo_index requires at least one field name"
|
|
@@ -304,13 +304,13 @@ module Parse
|
|
|
304
304
|
assert_at_most_one_array_field!(fields, wire_keys)
|
|
305
305
|
|
|
306
306
|
declaration = {
|
|
307
|
-
keys:
|
|
308
|
-
options:
|
|
307
|
+
keys: wire_keys,
|
|
308
|
+
options: {
|
|
309
309
|
unique: unique, sparse: sparse,
|
|
310
310
|
partial_filter: partial, expire_after: expire_after, name: name,
|
|
311
311
|
}.reject { |_, v| v.nil? || v == false }.freeze,
|
|
312
|
-
declared_for:
|
|
313
|
-
collection:
|
|
312
|
+
declared_for: fields.dup.freeze,
|
|
313
|
+
collection: nil, # nil sentinel means "use the model's parse_class"
|
|
314
314
|
}.freeze
|
|
315
315
|
|
|
316
316
|
# Idempotent redeclaration (same class re-opened or sub-class
|
|
@@ -325,10 +325,10 @@ module Parse
|
|
|
325
325
|
# model's `parse_class`.
|
|
326
326
|
def register_relation_index(collection, column, source:)
|
|
327
327
|
decl = {
|
|
328
|
-
keys:
|
|
329
|
-
options:
|
|
328
|
+
keys: { column => 1 }.freeze,
|
|
329
|
+
options: {}.freeze,
|
|
330
330
|
declared_for: [source].freeze,
|
|
331
|
-
collection:
|
|
331
|
+
collection: collection,
|
|
332
332
|
}.freeze
|
|
333
333
|
append_index_declaration(decl)
|
|
334
334
|
end
|
|
@@ -343,10 +343,10 @@ module Parse
|
|
|
343
343
|
# column (which `mongo_relation_index` continues to reject).
|
|
344
344
|
def register_relation_dedup_index(collection, source:)
|
|
345
345
|
decl = {
|
|
346
|
-
keys:
|
|
347
|
-
options:
|
|
346
|
+
keys: { "owningId" => 1, "relatedId" => 1 }.freeze,
|
|
347
|
+
options: { unique: true }.freeze,
|
|
348
348
|
declared_for: [source].freeze,
|
|
349
|
-
collection:
|
|
349
|
+
collection: collection,
|
|
350
350
|
}.freeze
|
|
351
351
|
append_index_declaration(decl)
|
|
352
352
|
end
|
|
@@ -185,7 +185,7 @@ module Parse
|
|
|
185
185
|
# `after_create` callback that issues a follow-up `update!`.
|
|
186
186
|
# @return [Symbol] the registered field name
|
|
187
187
|
def parse_reference(field_name = :parse_reference, field: nil, precompute: false,
|
|
188
|
-
|
|
188
|
+
index: true, unique_index: true)
|
|
189
189
|
field_name = field_name.to_sym
|
|
190
190
|
unless field_name.to_s =~ /\A[a-z_][a-z0-9_]*\z/i
|
|
191
191
|
raise ArgumentError,
|
|
@@ -159,6 +159,11 @@ module Parse
|
|
|
159
159
|
|
|
160
160
|
# @return [Hash] per-property metadata for `:vector`-typed fields.
|
|
161
161
|
# Maps property names (symbols) to a frozen options hash:
|
|
162
|
+
# Similarity functions Atlas vectorSearch accepts. Validated at
|
|
163
|
+
# declaration time so a typo surfaces when the class loads rather
|
|
164
|
+
# than as an Atlas index error much later.
|
|
165
|
+
VECTOR_SIMILARITIES = %w[euclidean cosine dotProduct].freeze
|
|
166
|
+
|
|
162
167
|
# `{ dimensions: Integer, provider: Symbol, model: String, similarity: Symbol }`.
|
|
163
168
|
# `dimensions:` is required; the rest are optional and only carry
|
|
164
169
|
# meaning for the embedding provider plumbing layered above this
|
|
@@ -240,7 +245,7 @@ module Parse
|
|
|
240
245
|
dropped = opts.keys.map(&:to_sym) - forwarded
|
|
241
246
|
unless dropped.empty?
|
|
242
247
|
warn "[#{self}] property #{key.inspect} resolves to a pointer association; " \
|
|
243
|
-
"ignoring unsupported option(s) #{dropped.map(&:inspect).join(
|
|
248
|
+
"ignoring unsupported option(s) #{dropped.map(&:inspect).join(", ")} " \
|
|
244
249
|
"(not available on belongs_to)."
|
|
245
250
|
end
|
|
246
251
|
return belongs_to(key, bt_opts)
|
|
@@ -396,11 +401,42 @@ module Parse
|
|
|
396
401
|
"Property #{self}##{key} :vector dimensions #{dims} exceeds max " \
|
|
397
402
|
"#{Parse::Vector::MAX_DIMENSIONS}."
|
|
398
403
|
end
|
|
404
|
+
|
|
405
|
+
# `searchable:` separates "can be stored" from "can be
|
|
406
|
+
# indexed". Parse::Vector tolerates up to 16384 dims, but
|
|
407
|
+
# Atlas caps a vectorSearch index at 8192 — so without this,
|
|
408
|
+
# a wider property is declarable, storable, and permanently
|
|
409
|
+
# unsearchable, with the failure surfacing only at query
|
|
410
|
+
# time. Default true: a :vector property exists to be
|
|
411
|
+
# searched, and opting out should be the deliberate act.
|
|
412
|
+
searchable = opts.key?(:searchable) ? opts[:searchable] : true
|
|
413
|
+
unless [true, false].include?(searchable)
|
|
414
|
+
raise ArgumentError,
|
|
415
|
+
"Property #{self}##{key} :vector `searchable:` must be true or false " \
|
|
416
|
+
"(got #{searchable.inspect})."
|
|
417
|
+
end
|
|
418
|
+
if searchable && defined?(Parse::VectorSearch) &&
|
|
419
|
+
dims > Parse::VectorSearch::MAX_DIMENSIONS
|
|
420
|
+
raise ArgumentError,
|
|
421
|
+
"Property #{self}##{key} :vector dimensions #{dims} exceeds the Atlas " \
|
|
422
|
+
"vectorSearch index cap (#{Parse::VectorSearch::MAX_DIMENSIONS}); such a " \
|
|
423
|
+
"vector can be stored but never searched. Reduce dimensions, or declare " \
|
|
424
|
+
"`searchable: false` to acknowledge it is storage-only."
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
similarity = opts[:similarity]
|
|
428
|
+
if similarity && !VECTOR_SIMILARITIES.include?(similarity.to_s)
|
|
429
|
+
raise ArgumentError,
|
|
430
|
+
"Property #{self}##{key} :vector `similarity:` must be one of " \
|
|
431
|
+
"#{VECTOR_SIMILARITIES.inspect} (got #{similarity.inspect})."
|
|
432
|
+
end
|
|
433
|
+
|
|
399
434
|
vector_properties[key] = {
|
|
400
435
|
dimensions: dims,
|
|
401
436
|
provider: opts[:provider],
|
|
402
437
|
model: opts[:model],
|
|
403
|
-
similarity:
|
|
438
|
+
similarity: similarity,
|
|
439
|
+
searchable: searchable,
|
|
404
440
|
}.freeze
|
|
405
441
|
|
|
406
442
|
validates_each key do |record, attribute, value|
|
|
@@ -411,7 +447,7 @@ module Parse
|
|
|
411
447
|
expected = record.class.vector_properties.dig(attribute, :dimensions)
|
|
412
448
|
if expected && value.dimensions != expected
|
|
413
449
|
record.errors.add(attribute,
|
|
414
|
-
|
|
450
|
+
"field :#{attribute} expected #{expected} dimensions, got #{value.dimensions}.")
|
|
415
451
|
end
|
|
416
452
|
end
|
|
417
453
|
end # validates_each
|
|
@@ -613,7 +613,7 @@ module Parse
|
|
|
613
613
|
# Forward session-token / use_master_key when supplied so client-mode
|
|
614
614
|
# callers can scope a `.find` to a logged-in user without dropping
|
|
615
615
|
# down to the raw `client.fetch_object` form.
|
|
616
|
-
client_opts[:session_token]
|
|
616
|
+
client_opts[:session_token] = session_token unless session_token.nil?
|
|
617
617
|
client_opts[:use_master_key] = use_master_key unless use_master_key.nil?
|
|
618
618
|
# The parallel path spawns worker threads via `Parallel.map`. Worker
|
|
619
619
|
# threads don't inherit fiber-local storage from the calling thread,
|
|
@@ -72,7 +72,7 @@ module Parse
|
|
|
72
72
|
# These are managed automatically by Parse Server.
|
|
73
73
|
SCHEMA_READONLY_CLASSES = [
|
|
74
74
|
Parse::Model::CLASS_PUSH_STATUS,
|
|
75
|
-
Parse::Model::CLASS_SCHEMA
|
|
75
|
+
Parse::Model::CLASS_SCHEMA,
|
|
76
76
|
].freeze
|
|
77
77
|
|
|
78
78
|
# Default CLP that grants public access to all operations.
|
|
@@ -84,7 +84,7 @@ module Parse
|
|
|
84
84
|
"create" => { "*" => true },
|
|
85
85
|
"update" => { "*" => true },
|
|
86
86
|
"delete" => { "*" => true },
|
|
87
|
-
"addField" => { "*" => true }
|
|
87
|
+
"addField" => { "*" => true },
|
|
88
88
|
}.freeze
|
|
89
89
|
|
|
90
90
|
# Reset the CLP on the server to public defaults.
|
|
@@ -99,9 +99,9 @@ module Parse
|
|
|
99
99
|
end
|
|
100
100
|
|
|
101
101
|
declaration = {
|
|
102
|
-
name:
|
|
102
|
+
name: name_str,
|
|
103
103
|
definition: deep_freeze(definition),
|
|
104
|
-
type:
|
|
104
|
+
type: type_str,
|
|
105
105
|
}.freeze
|
|
106
106
|
|
|
107
107
|
existing = mongo_search_index_declarations.find { |d| d[:name] == name_str }
|