parse-stack-next 5.6.0 → 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 +409 -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 +7 -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/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 +5 -4
- data/lib/parse/embeddings/openai.rb +5 -5
- data/lib/parse/embeddings/provider.rb +1 -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 +11 -11
- data/lib/parse/embeddings/video_source.rb +7 -6
- data/lib/parse/embeddings/voyage.rb +59 -61
- data/lib/parse/embeddings.rb +28 -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 +21 -23
- 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 +2 -2
- 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 +4 -5
- 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 +66 -41
- data/lib/parse/vector_search.rb +14 -11
- 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 +10 -1
|
@@ -109,6 +109,100 @@ module Parse
|
|
|
109
109
|
end
|
|
110
110
|
end
|
|
111
111
|
|
|
112
|
+
# Capture the property values a transaction rollback must restore.
|
|
113
|
+
#
|
|
114
|
+
# Property values live in `@<field>` instance variables (see
|
|
115
|
+
# {Parse::Properties::ClassMethods#property}), so those are what gets
|
|
116
|
+
# snapshotted. {Parse::Object#attributes} is deliberately NOT captured:
|
|
117
|
+
# it returns a SCHEMA map (field name to type symbol), not values, so
|
|
118
|
+
# restoring it never rolled anything back.
|
|
119
|
+
#
|
|
120
|
+
# Restoring it was also actively harmful. `@attributes` is the ivar
|
|
121
|
+
# `ActiveModel::Dirty` keys its behavior on: `mutations_from_database`
|
|
122
|
+
# builds an `AttributeMutationTracker` over `@attributes` the moment
|
|
123
|
+
# that ivar is defined (and a `ForcedMutationTracker` otherwise), while
|
|
124
|
+
# `forget_attribute_assignments` calls `map(&:forgetting_assignment)`
|
|
125
|
+
# on it. Handing either one a schema Hash raised `NoMethodError` on a
|
|
126
|
+
# type symbol and on the `[key, value]` pair `Hash#map` yields. Both
|
|
127
|
+
# sites rescue and warn, so every rollback silently downgraded the
|
|
128
|
+
# object to broken change tracking instead of failing.
|
|
129
|
+
#
|
|
130
|
+
# @param obj [Parse::Object] the object being tracked.
|
|
131
|
+
# @return [Hash] field name to a snapshot of its value. Mutable values
|
|
132
|
+
# are duplicated so in-place mutation between snapshot and rollback
|
|
133
|
+
# cannot corrupt the saved copy.
|
|
134
|
+
def self.snapshot_property_values(obj)
|
|
135
|
+
fields = obj.class.respond_to?(:fields) ? obj.class.fields.keys : []
|
|
136
|
+
fields.each_with_object({}) do |key, snapshot|
|
|
137
|
+
ivar = :"@#{key}"
|
|
138
|
+
next unless obj.instance_variable_defined?(ivar)
|
|
139
|
+
snapshot[ivar] = dup_for_snapshot(obj.instance_variable_get(ivar))
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Copy a property value deeply enough that later in-place mutation of
|
|
144
|
+
# the live value cannot reach the snapshot.
|
|
145
|
+
#
|
|
146
|
+
# A plain `dup` is not sufficient for collection-backed properties:
|
|
147
|
+
# `:array` and association properties hold a {Parse::CollectionProxy},
|
|
148
|
+
# and duplicating the proxy still shares the underlying `@collection`
|
|
149
|
+
# array, so `widget.tags << "b"` would mutate the snapshot too. The
|
|
150
|
+
# proxy's inner array is duplicated as well.
|
|
151
|
+
#
|
|
152
|
+
# @param value [Object] the live property value.
|
|
153
|
+
# @return [Object] a copy safe to hold across the transaction.
|
|
154
|
+
def self.dup_for_snapshot(value)
|
|
155
|
+
copy = begin
|
|
156
|
+
value.dup
|
|
157
|
+
rescue TypeError
|
|
158
|
+
# Symbols, Integers, true/false/nil and other immediates are not
|
|
159
|
+
# duplicable on older rubies; they are also immutable, so sharing
|
|
160
|
+
# the reference is safe.
|
|
161
|
+
return value
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
if copy.instance_variable_defined?(:@collection)
|
|
165
|
+
inner = copy.instance_variable_get(:@collection)
|
|
166
|
+
copy.instance_variable_set(:@collection, inner.dup) if inner.is_a?(Array)
|
|
167
|
+
end
|
|
168
|
+
copy
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Restore the values captured by {snapshot_property_values}.
|
|
172
|
+
#
|
|
173
|
+
# Writes ivars directly rather than going through the property setters,
|
|
174
|
+
# since the setters mark the object dirty and the caller restores the
|
|
175
|
+
# dirty state itself immediately afterwards.
|
|
176
|
+
#
|
|
177
|
+
# @param obj [Parse::Object] the object being rolled back.
|
|
178
|
+
# @param snapshot [Hash] the return value of {snapshot_property_values}.
|
|
179
|
+
# @return [void]
|
|
180
|
+
def self.restore_property_values(obj, snapshot)
|
|
181
|
+
return unless snapshot.is_a?(Hash)
|
|
182
|
+
snapshot.each do |ivar, value|
|
|
183
|
+
obj.instance_variable_set(ivar, value)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Roll a single tracked object back to the state captured when it was
|
|
188
|
+
# added to the transaction.
|
|
189
|
+
#
|
|
190
|
+
# @param state [Hash] one entry of the transaction's `original_states`.
|
|
191
|
+
# @return [void]
|
|
192
|
+
def self.rollback_object_state(state)
|
|
193
|
+
obj = state[:object]
|
|
194
|
+
return if obj.nil?
|
|
195
|
+
restore_property_values(obj, state[:property_values])
|
|
196
|
+
obj.instance_variable_set(:@changed_attributes, state[:changed_attributes])
|
|
197
|
+
obj.instance_variable_set(:@id, state[:id])
|
|
198
|
+
# Restore change tracking state. Leaving `@mutations_from_database`
|
|
199
|
+
# nil is fine: `ActiveModel::Dirty` lazily rebuilds it, and with
|
|
200
|
+
# `@attributes` no longer defined on the object it correctly rebuilds
|
|
201
|
+
# a `ForcedMutationTracker`.
|
|
202
|
+
obj.instance_variable_set(:@mutations_from_database, state[:mutations_from_database])
|
|
203
|
+
obj.instance_variable_set(:@mutations_before_last_save, state[:mutations_before_last_save])
|
|
204
|
+
end
|
|
205
|
+
|
|
112
206
|
# Class methods applied to Parse::Object subclasses.
|
|
113
207
|
module ClassMethods
|
|
114
208
|
|
|
@@ -169,7 +263,7 @@ module Parse
|
|
|
169
263
|
if obj.respond_to?(:attributes) && obj.respond_to?(:id) && !original_states.key?(obj.object_id)
|
|
170
264
|
original_states[obj.object_id] = {
|
|
171
265
|
object: obj,
|
|
172
|
-
|
|
266
|
+
property_values: Parse::Core::Actions.snapshot_property_values(obj),
|
|
173
267
|
changed_attributes: obj.instance_variable_get(:@changed_attributes)&.dup || {},
|
|
174
268
|
id: obj.id,
|
|
175
269
|
mutations_from_database: obj.instance_variable_get(:@mutations_from_database),
|
|
@@ -243,13 +337,7 @@ module Parse
|
|
|
243
337
|
|
|
244
338
|
# Rollback local object states
|
|
245
339
|
original_states.each_value do |state|
|
|
246
|
-
|
|
247
|
-
obj.instance_variable_set(:@attributes, state[:attributes])
|
|
248
|
-
obj.instance_variable_set(:@changed_attributes, state[:changed_attributes])
|
|
249
|
-
obj.instance_variable_set(:@id, state[:id])
|
|
250
|
-
# Restore change tracking state
|
|
251
|
-
obj.instance_variable_set(:@mutations_from_database, state[:mutations_from_database])
|
|
252
|
-
obj.instance_variable_set(:@mutations_before_last_save, state[:mutations_before_last_save])
|
|
340
|
+
Parse::Core::Actions.rollback_object_state(state)
|
|
253
341
|
end
|
|
254
342
|
|
|
255
343
|
raise Parse::Error, "Transaction failed: #{error_response.error}"
|
|
@@ -263,13 +351,7 @@ module Parse
|
|
|
263
351
|
|
|
264
352
|
# Rollback local object states on final failure
|
|
265
353
|
original_states.each_value do |state|
|
|
266
|
-
|
|
267
|
-
obj.instance_variable_set(:@attributes, state[:attributes])
|
|
268
|
-
obj.instance_variable_set(:@changed_attributes, state[:changed_attributes])
|
|
269
|
-
obj.instance_variable_set(:@id, state[:id])
|
|
270
|
-
# Restore change tracking state
|
|
271
|
-
obj.instance_variable_set(:@mutations_from_database, state[:mutations_from_database])
|
|
272
|
-
obj.instance_variable_set(:@mutations_before_last_save, state[:mutations_before_last_save])
|
|
354
|
+
Parse::Core::Actions.rollback_object_state(state)
|
|
273
355
|
end
|
|
274
356
|
|
|
275
357
|
raise e
|
|
@@ -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,
|
|
@@ -649,10 +648,10 @@ module Parse
|
|
|
649
648
|
# explicit `audit_all!` path apply identical rules.
|
|
650
649
|
def self.audit_binding!(klass, directive)
|
|
651
650
|
provider = begin
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
651
|
+
Parse::Embeddings.provider(directive.provider_name)
|
|
652
|
+
rescue Parse::Embeddings::ProviderNotRegistered
|
|
653
|
+
return
|
|
654
|
+
end
|
|
656
655
|
Parse::Embeddings::BindingAudit.verify!(klass, directive.into, provider)
|
|
657
656
|
end
|
|
658
657
|
|
|
@@ -720,10 +719,10 @@ module Parse
|
|
|
720
719
|
return if directive.meta_field.nil?
|
|
721
720
|
return unless record.respond_to?(:"#{directive.meta_field}=")
|
|
722
721
|
model = begin
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
722
|
+
provider.model_name
|
|
723
|
+
rescue NotImplementedError
|
|
724
|
+
nil
|
|
725
|
+
end
|
|
727
726
|
record.public_send(:"#{directive.meta_field}=", {
|
|
728
727
|
"provider" => directive.provider_name.to_s,
|
|
729
728
|
"model" => model,
|
|
@@ -777,8 +776,7 @@ module Parse
|
|
|
777
776
|
# fetches it itself).
|
|
778
777
|
def self.call_provider(provider, directive, input)
|
|
779
778
|
if directive.image?
|
|
780
|
-
source =
|
|
781
|
-
if directive.bytes_mode?
|
|
779
|
+
source = if directive.bytes_mode?
|
|
782
780
|
Parse::Embeddings::ImageFetch.fetch!(
|
|
783
781
|
input,
|
|
784
782
|
allow_insecure: directive.allow_insecure ? true : false,
|
|
@@ -788,8 +786,8 @@ module Parse
|
|
|
788
786
|
input
|
|
789
787
|
end
|
|
790
788
|
provider.embed_image([source],
|
|
791
|
-
|
|
792
|
-
|
|
789
|
+
input_type: directive.input_type,
|
|
790
|
+
allow_insecure: directive.allow_insecure ? true : false)
|
|
793
791
|
else
|
|
794
792
|
provider.embed_text([input], input_type: directive.input_type)
|
|
795
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,
|
|
@@ -245,7 +245,7 @@ module Parse
|
|
|
245
245
|
dropped = opts.keys.map(&:to_sym) - forwarded
|
|
246
246
|
unless dropped.empty?
|
|
247
247
|
warn "[#{self}] property #{key.inspect} resolves to a pointer association; " \
|
|
248
|
-
"ignoring unsupported option(s) #{dropped.map(&:inspect).join(
|
|
248
|
+
"ignoring unsupported option(s) #{dropped.map(&:inspect).join(", ")} " \
|
|
249
249
|
"(not available on belongs_to)."
|
|
250
250
|
end
|
|
251
251
|
return belongs_to(key, bt_opts)
|
|
@@ -447,7 +447,7 @@ module Parse
|
|
|
447
447
|
expected = record.class.vector_properties.dig(attribute, :dimensions)
|
|
448
448
|
if expected && value.dimensions != expected
|
|
449
449
|
record.errors.add(attribute,
|
|
450
|
-
|
|
450
|
+
"field :#{attribute} expected #{expected} dimensions, got #{value.dimensions}.")
|
|
451
451
|
end
|
|
452
452
|
end
|
|
453
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 }
|