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.
Files changed (118) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +409 -0
  3. data/README.md +378 -6
  4. data/docs/caching.md +748 -0
  5. data/examples/basic_client.rb +3 -3
  6. data/examples/basic_server.rb +3 -3
  7. data/examples/live_query_listener.rb +12 -12
  8. data/examples/rag_chatbot.rb +14 -14
  9. data/examples/transaction_example.rb +44 -45
  10. data/examples/webhook_server.rb +3 -3
  11. data/lib/parse/access.rb +493 -0
  12. data/lib/parse/acl_scope.rb +125 -52
  13. data/lib/parse/agent/approval_gate.rb +0 -0
  14. data/lib/parse/agent/cancellation_token.rb +3 -3
  15. data/lib/parse/agent/constraint_translator.rb +4 -4
  16. data/lib/parse/agent/describe.rb +34 -34
  17. data/lib/parse/agent/errors.rb +9 -9
  18. data/lib/parse/agent/mcp_client.rb +61 -59
  19. data/lib/parse/agent/mcp_dispatcher.rb +89 -101
  20. data/lib/parse/agent/mcp_rack_app.rb +92 -93
  21. data/lib/parse/agent/mcp_server.rb +1 -1
  22. data/lib/parse/agent/mcp_subscriptions.rb +22 -22
  23. data/lib/parse/agent/metadata_audit.rb +1 -2
  24. data/lib/parse/agent/metadata_dsl.rb +7 -7
  25. data/lib/parse/agent/metadata_registry.rb +12 -12
  26. data/lib/parse/agent/prompt_hardening.rb +4 -4
  27. data/lib/parse/agent/prompts.rb +10 -10
  28. data/lib/parse/agent/result_formatter.rb +4 -4
  29. data/lib/parse/agent/tools.rb +493 -400
  30. data/lib/parse/agent.rb +157 -116
  31. data/lib/parse/api/hooks.rb +1 -1
  32. data/lib/parse/api/server.rb +2 -2
  33. data/lib/parse/api/users.rb +2 -2
  34. data/lib/parse/atlas_search/index_manager.rb +1 -1
  35. data/lib/parse/atlas_search/session.rb +40 -218
  36. data/lib/parse/atlas_search.rb +181 -60
  37. data/lib/parse/authorization.rb +466 -0
  38. data/lib/parse/cache/invalidation.rb +167 -0
  39. data/lib/parse/cache/keyspace.rb +306 -0
  40. data/lib/parse/cache/moneta_surface.rb +126 -0
  41. data/lib/parse/cache/pool.rb +48 -5
  42. data/lib/parse/cache/redis.rb +425 -10
  43. data/lib/parse/cache/scoped_view.rb +443 -0
  44. data/lib/parse/cache/sub_cache.rb +264 -0
  45. data/lib/parse/cache/upstream_roles.rb +230 -0
  46. data/lib/parse/client/authentication.rb +1 -1
  47. data/lib/parse/client/body_builder.rb +1 -2
  48. data/lib/parse/client/caching.rb +84 -5
  49. data/lib/parse/client.rb +327 -189
  50. data/lib/parse/clp_scope.rb +225 -28
  51. data/lib/parse/console.rb +3 -3
  52. data/lib/parse/embeddings/batch_embedder.rb +3 -3
  53. data/lib/parse/embeddings/cache.rb +17 -17
  54. data/lib/parse/embeddings/cohere.rb +21 -22
  55. data/lib/parse/embeddings/image_fetch.rb +22 -21
  56. data/lib/parse/embeddings/jina.rb +21 -21
  57. data/lib/parse/embeddings/local_http.rb +6 -7
  58. data/lib/parse/embeddings/media_file.rb +5 -4
  59. data/lib/parse/embeddings/openai.rb +5 -5
  60. data/lib/parse/embeddings/provider.rb +1 -1
  61. data/lib/parse/embeddings/qwen.rb +10 -10
  62. data/lib/parse/embeddings/spend_cap.rb +2 -3
  63. data/lib/parse/embeddings/streaming_body.rb +11 -11
  64. data/lib/parse/embeddings/video_source.rb +7 -6
  65. data/lib/parse/embeddings/voyage.rb +59 -61
  66. data/lib/parse/embeddings.rb +28 -27
  67. data/lib/parse/graphql/type_generator.rb +1 -1
  68. data/lib/parse/graphql.rb +5 -5
  69. data/lib/parse/live_query/client.rb +1 -1
  70. data/lib/parse/live_query.rb +1 -1
  71. data/lib/parse/lock.rb +15 -18
  72. data/lib/parse/lock_backend.rb +1 -1
  73. data/lib/parse/lookup_rewriter.rb +6 -6
  74. data/lib/parse/model/classes/role.rb +227 -28
  75. data/lib/parse/model/classes/user.rb +60 -1
  76. data/lib/parse/model/clp.rb +4 -4
  77. data/lib/parse/model/core/actions.rb +97 -15
  78. data/lib/parse/model/core/create_lock.rb +0 -2
  79. data/lib/parse/model/core/describe.rb +57 -57
  80. data/lib/parse/model/core/embed_managed.rb +21 -23
  81. data/lib/parse/model/core/fetching.rb +2 -0
  82. data/lib/parse/model/core/indexing.rb +14 -14
  83. data/lib/parse/model/core/parse_reference.rb +1 -1
  84. data/lib/parse/model/core/properties.rb +2 -2
  85. data/lib/parse/model/core/querying.rb +1 -1
  86. data/lib/parse/model/core/schema.rb +2 -2
  87. data/lib/parse/model/core/search_indexing.rb +2 -2
  88. data/lib/parse/model/core/vector_searchable.rb +4 -5
  89. data/lib/parse/model/file.rb +21 -15
  90. data/lib/parse/model/geojson.rb +2 -2
  91. data/lib/parse/model/geopoint.rb +1 -0
  92. data/lib/parse/model/object.rb +132 -66
  93. data/lib/parse/model/pointer.rb +2 -0
  94. data/lib/parse/model/polygon.rb +3 -6
  95. data/lib/parse/model/push.rb +2 -2
  96. data/lib/parse/model/vector.rb +3 -1
  97. data/lib/parse/mongodb.rb +512 -282
  98. data/lib/parse/pipeline_security.rb +3 -0
  99. data/lib/parse/query/constraints.rb +46 -46
  100. data/lib/parse/query.rb +146 -59
  101. data/lib/parse/retrieval/agent_tool.rb +34 -20
  102. data/lib/parse/retrieval/chunk.rb +1 -0
  103. data/lib/parse/retrieval/reranker/cohere.rb +11 -11
  104. data/lib/parse/retrieval/reranker.rb +3 -4
  105. data/lib/parse/retrieval/retriever.rb +4 -6
  106. data/lib/parse/schema/index_migrator.rb +26 -26
  107. data/lib/parse/schema/search_index_migrator.rb +19 -19
  108. data/lib/parse/stack/tasks.rb +6 -6
  109. data/lib/parse/stack/version.rb +1 -1
  110. data/lib/parse/stack.rb +10 -10
  111. data/lib/parse/vector_search/hybrid.rb +66 -41
  112. data/lib/parse/vector_search.rb +14 -11
  113. data/lib/parse/webhooks/payload.rb +1 -0
  114. data/lib/parse/webhooks/registration.rb +4 -4
  115. data/lib/parse/webhooks/trigger_audit.rb +50 -49
  116. data/lib/parse/webhooks.rb +22 -4
  117. data/parse-stack-next.gemspec +6 -6
  118. 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
- attributes: obj.attributes.dup,
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
- obj = state[:object]
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
- obj = state[:object]
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
@@ -276,5 +276,3 @@ module Parse
276
276
  end
277
277
  end
278
278
  end
279
-
280
-
@@ -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 = %i[model acl].freeze
21
+ LOCAL_SECTIONS = %i[model acl].freeze
22
22
  NETWORK_SECTIONS = %i[schema clp atlas indexes].freeze
23
- ALL_SECTIONS = (LOCAL_SECTIONS + NETWORK_SECTIONS).freeze
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 = if requested.empty?
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 then describe_model_section
76
- when :acl then describe_acl_section
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 then network_section(network) { describe_clp_section(client) }
79
- when :atlas then network_section(network) { describe_atlas_section }
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 { available: false, reason: :unknown_section }
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: 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),
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: defaults.respond_to?(:as_json) ? defaults.as_json : nil,
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: instance_variable_get(:@acl_policy_setting),
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: true,
130
- in_sync: diff.in_sync?,
129
+ available: true,
130
+ in_sync: diff.in_sync?,
131
131
  server_field_count: info.field_names.size,
132
- missing_on_server: diff.missing_on_server,
133
- missing_locally: diff.missing_locally,
134
- type_mismatches: diff.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: indexes.size,
152
- indexes: indexes.map { |i|
153
- { name: i["name"],
154
- status: i["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: normalized.size,
187
- indexes: normalized,
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 = parse_class
206
+ base = parse_class
207
207
  parent_plan = plans[base]
208
208
  if parent_plan
209
- result[:declared] = parent_plan[:declared].map { |d| describe_decl(d) }
210
- result[:drift] = describe_drift(parent_plan)
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] = describe_capacity(parent_plan)
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: p[:declared].map { |d| describe_decl(d) },
219
- drift: describe_drift(p),
218
+ declared: p[:declared].map { |d| describe_decl(d) },
219
+ drift: describe_drift(p),
220
220
  parse_managed: p[:parse_managed],
221
- capacity: describe_capacity(p),
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: plan[:in_sync].map { |d| describe_decl(d) },
238
- orphans: plan[: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: plan[:capacity_used],
246
- after: plan[:capacity_after],
245
+ used: plan[:capacity_used],
246
+ after: plan[:capacity_after],
247
247
  remaining: plan[:capacity_remaining],
248
- ok: plan[:capacity_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: name,
263
- implicit_id: name == "_id_",
264
- key: coerce_bson(idx["key"] || idx[:key] || {}),
265
- unique: idx["unique"] == true,
266
- sparse: idx["sparse"] == true,
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}" if m[:relations].any?
296
- lines << " defaults: #{m[:defaults].inspect}" if m[:defaults].any?
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}" if m[:agent_fields]
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}" if s[:missing_locally].any?
313
- lines << " type_mismatches: #{s[:type_mismatches].keys.inspect}" if s[:type_mismatches].any?
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}" 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?
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}" if d[:in_sync].any?
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
- meta_field: nil)
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
- digest_field: nil, allow_insecure: false,
329
- source: :url, exif_strip: true, meta_field: nil)
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
- Parse::Embeddings.provider(directive.provider_name).model_name
493
- rescue Parse::Embeddings::ProviderNotRegistered
494
- raise
495
- rescue NotImplementedError
496
- nil
497
- end
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
- Parse::Embeddings.provider(directive.provider_name)
653
- rescue Parse::Embeddings::ProviderNotRegistered
654
- return
655
- end
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
- provider.model_name
724
- rescue NotImplementedError
725
- nil
726
- end
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
- input_type: directive.input_type,
792
- allow_insecure: directive.allow_insecure ? true : false)
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
- expire_after: nil, name: nil)
96
+ expire_after: nil, name: nil)
97
97
  register_index(fields, key_value: 1, unique: unique, sparse: sparse,
98
- partial: partial, expire_after: expire_after, name: name)
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
- sparse: sparse, partial: nil, expire_after: nil, name: name)
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
- expire_after:, name:)
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: wire_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: fields.dup.freeze,
313
- collection: nil, # nil sentinel means "use the model's parse_class"
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: { column => 1 }.freeze,
329
- options: {}.freeze,
328
+ keys: { column => 1 }.freeze,
329
+ options: {}.freeze,
330
330
  declared_for: [source].freeze,
331
- collection: 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: { "owningId" => 1, "relatedId" => 1 }.freeze,
347
- options: { unique: true }.freeze,
346
+ keys: { "owningId" => 1, "relatedId" => 1 }.freeze,
347
+ options: { unique: true }.freeze,
348
348
  declared_for: [source].freeze,
349
- collection: 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
- index: true, unique_index: true)
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
- "field :#{attribute} expected #{expected} dimensions, got #{value.dimensions}.")
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] = session_token unless session_token.nil?
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: name_str,
102
+ name: name_str,
103
103
  definition: deep_freeze(definition),
104
- type: type_str,
104
+ type: type_str,
105
105
  }.freeze
106
106
 
107
107
  existing = mongo_search_index_declarations.find { |d| d[:name] == name_str }