parse-stack-next 5.7.6 → 5.8.1
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 +1148 -0
- data/README.md +39 -23
- data/docs/TEST_SERVER.md +2 -2
- data/docs/acl_clp_guide.md +7 -0
- data/docs/atlas_vector_search_guide.md +181 -13
- data/docs/caching.md +50 -0
- data/docs/client_sdk_guide.md +11 -0
- data/docs/mcp_guide.md +332 -9
- data/docs/mongodb_direct_guide.md +27 -0
- data/docs/usage_guide.md +38 -0
- data/docs/webhooks_guide.md +126 -17
- data/lib/parse/acl_scope.rb +230 -50
- data/lib/parse/agent/approval_gate.rb +0 -0
- data/lib/parse/agent/constraint_translator.rb +42 -15
- data/lib/parse/agent/describe.rb +11 -3
- data/lib/parse/agent/field_names.rb +53 -0
- data/lib/parse/agent/field_policy.rb +74 -0
- data/lib/parse/agent/mcp_deployments.rb +426 -0
- data/lib/parse/agent/mcp_dispatcher.rb +8 -1
- data/lib/parse/agent/mcp_rack_app.rb +424 -45
- data/lib/parse/agent/mcp_server.rb +23 -1
- data/lib/parse/agent/mcp_subscriptions.rb +124 -6
- data/lib/parse/agent/metadata_registry.rb +67 -8
- data/lib/parse/agent/prompt_hardening.rb +9 -3
- data/lib/parse/agent/tools.rb +378 -29
- data/lib/parse/agent.rb +298 -21
- data/lib/parse/api/batch.rb +10 -1
- data/lib/parse/api/schema.rb +23 -4
- data/lib/parse/api/sessions.rb +6 -2
- data/lib/parse/api/users.rb +88 -14
- data/lib/parse/atlas_search/index_manager.rb +5 -1
- data/lib/parse/atlas_search/protected_paths.rb +236 -0
- data/lib/parse/atlas_search.rb +144 -24
- data/lib/parse/authorization.rb +289 -4
- data/lib/parse/cache/sub_cache.rb +26 -0
- data/lib/parse/client/authentication.rb +19 -1
- data/lib/parse/client/batch.rb +264 -35
- data/lib/parse/client/body_builder.rb +21 -0
- data/lib/parse/client/caching.rb +371 -27
- data/lib/parse/client/request.rb +26 -14
- data/lib/parse/client/response.rb +49 -6
- data/lib/parse/client.rb +227 -40
- data/lib/parse/clp_scope.rb +317 -25
- data/lib/parse/console.rb +2 -2
- data/lib/parse/embeddings/voyage.rb +181 -17
- data/lib/parse/graphql/type_generator.rb +3 -0
- data/lib/parse/live_query/client.rb +72 -2
- data/lib/parse/lock_backend.rb +4 -3
- data/lib/parse/model/acl.rb +119 -21
- data/lib/parse/model/associations/belongs_to.rb +25 -3
- data/lib/parse/model/associations/collection_proxy.rb +146 -17
- data/lib/parse/model/associations/has_many.rb +49 -9
- data/lib/parse/model/associations/has_one.rb +3 -1
- data/lib/parse/model/associations/pointer_collection_proxy.rb +145 -17
- data/lib/parse/model/associations/relation_collection_proxy.rb +229 -38
- data/lib/parse/model/bytes.rb +13 -5
- data/lib/parse/model/classes/role.rb +95 -0
- data/lib/parse/model/classes/session.rb +304 -1
- data/lib/parse/model/classes/user.rb +104 -8
- data/lib/parse/model/core/actions.rb +287 -70
- data/lib/parse/model/core/builder.rb +100 -8
- data/lib/parse/model/core/create_lock.rb +27 -2
- data/lib/parse/model/core/describe.rb +2 -0
- data/lib/parse/model/core/fetching.rb +50 -5
- data/lib/parse/model/core/field_guards.rb +16 -8
- data/lib/parse/model/core/pluralized_aliases.rb +8 -4
- data/lib/parse/model/core/properties.rb +488 -39
- data/lib/parse/model/core/querying.rb +7 -0
- data/lib/parse/model/core/schema.rb +5 -3
- data/lib/parse/model/core/search_indexing.rb +63 -0
- data/lib/parse/model/core/vector_searchable.rb +35 -6
- data/lib/parse/model/file.rb +9 -2
- data/lib/parse/model/geopoint.rb +61 -13
- data/lib/parse/model/model.rb +160 -9
- data/lib/parse/model/object.rb +317 -18
- data/lib/parse/model/phone.rb +54 -5
- data/lib/parse/model/pointer.rb +40 -6
- data/lib/parse/model/push.rb +11 -1
- data/lib/parse/mongodb.rb +189 -61
- data/lib/parse/pipeline_security.rb +415 -26
- data/lib/parse/query/constraint.rb +31 -1
- data/lib/parse/query/constraints.rb +89 -59
- data/lib/parse/query/cursor.rb +3 -1
- data/lib/parse/query/operation.rb +62 -8
- data/lib/parse/query/ordering.rb +34 -6
- data/lib/parse/query.rb +1871 -167
- data/lib/parse/retrieval/agent_tool.rb +225 -8
- data/lib/parse/retrieval/benchmark.rb +149 -0
- data/lib/parse/retrieval/profiles.rb +320 -0
- data/lib/parse/retrieval/retriever.rb +10 -1
- data/lib/parse/retrieval.rb +2 -0
- data/lib/parse/schema/search_index_migrator.rb +23 -5
- data/lib/parse/schema.rb +74 -18
- data/lib/parse/stack/tasks.rb +6 -4
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +91 -14
- data/lib/parse/two_factor_auth/user_extension.rb +14 -2
- data/lib/parse/two_factor_auth.rb +11 -0
- data/lib/parse/vector_search/hybrid.rb +110 -33
- data/lib/parse/vector_search/index_definition.rb +237 -0
- data/lib/parse/vector_search.rb +259 -23
- data/lib/parse/webhooks/payload.rb +136 -6
- data/lib/parse/webhooks/replay_protection.rb +58 -20
- data/lib/parse/webhooks.rb +715 -40
- metadata +8 -1
|
@@ -89,6 +89,9 @@ module Parse
|
|
|
89
89
|
if klass.nil?
|
|
90
90
|
klass = ::Class.new(Parse::Object)
|
|
91
91
|
Parse::Generated.const_set(parse_class_name, klass)
|
|
92
|
+
# The default parse_class comes from the Ruby name, which here is
|
|
93
|
+
# namespaced ("Parse::Generated::Post"). Bind the server class name.
|
|
94
|
+
klass.parse_class(className.to_s)
|
|
92
95
|
end
|
|
93
96
|
unless klass.is_a?(Class) && klass <= Parse::Object
|
|
94
97
|
raise ArgumentError, "Resolved class #{klass.inspect} for #{className.inspect} is not a Parse::Object subclass"
|
|
@@ -106,22 +109,111 @@ module Parse
|
|
|
106
109
|
class_fields = klass.field_map.values + [:className]
|
|
107
110
|
fields.each do |field, type|
|
|
108
111
|
field = field.to_sym
|
|
109
|
-
key = field.to_s.underscore.to_sym
|
|
110
112
|
next if base_fields.include?(field) || class_fields.include?(field)
|
|
113
|
+
next unless type.respond_to?(:[]) && type[:type].present?
|
|
111
114
|
|
|
112
|
-
data_type = type[:type].downcase.to_sym
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
115
|
+
data_type = type[:type].to_s.downcase.to_sym
|
|
116
|
+
# A model's field registry shares one namespace between Ruby
|
|
117
|
+
# names and server columns, so a column whose name another column
|
|
118
|
+
# already claimed as its Ruby name (`foo_bar` after `fooBar`)
|
|
119
|
+
# cannot be mapped. Skip it instead of aborting the whole build.
|
|
120
|
+
if klass.fields.key?(field) || klass.field_map.key?(field)
|
|
121
|
+
builder_warn "skipping column #{className}.#{field}: its name is already " \
|
|
122
|
+
"used by another column of the same class"
|
|
123
|
+
next
|
|
124
|
+
end
|
|
125
|
+
key = safe_accessor_name(klass, field, data_type)
|
|
126
|
+
next if key.nil?
|
|
127
|
+
|
|
128
|
+
begin
|
|
129
|
+
if data_type == :pointer
|
|
130
|
+
klass.belongs_to key, as: safe_target_class(type[:targetClass]), field: field
|
|
131
|
+
elsif data_type == :relation
|
|
132
|
+
klass.has_many key, through: :relation, as: safe_target_class(type[:targetClass]), field: field
|
|
133
|
+
else
|
|
134
|
+
# A renamed accessor must not get an alias under the column
|
|
135
|
+
# name, which is the method it was renamed to avoid.
|
|
136
|
+
opts = { field: field }
|
|
137
|
+
opts[:alias] = false if key.to_s != field.to_s.underscore
|
|
138
|
+
klass.property key, data_type, **opts
|
|
139
|
+
end
|
|
140
|
+
rescue StandardError, SystemStackError => e
|
|
141
|
+
builder_warn "skipping column #{className}.#{field}: #{e.class}: #{e.message}"
|
|
142
|
+
next
|
|
143
|
+
end
|
|
144
|
+
# Hydration from server JSON dispatches on `<column>_set_attribute!`.
|
|
145
|
+
# The association and property DSLs only alias that hook when the
|
|
146
|
+
# column name itself is free as a method, so add it for a renamed
|
|
147
|
+
# accessor (`class` -> `class_field`).
|
|
148
|
+
setter = :"#{field}_set_attribute!"
|
|
149
|
+
target = :"#{key}_set_attribute!"
|
|
150
|
+
if setter != target && !klass.method_defined?(setter) && klass.method_defined?(target)
|
|
151
|
+
klass.send(:alias_method, setter, target)
|
|
119
152
|
end
|
|
120
153
|
class_fields.push(field)
|
|
121
154
|
end
|
|
122
155
|
klass
|
|
123
156
|
end
|
|
124
157
|
|
|
158
|
+
# Suffix appended to a column's Ruby accessor name when the natural
|
|
159
|
+
# (underscored) name is unusable.
|
|
160
|
+
RENAMED_ACCESSOR_SUFFIX = "_field"
|
|
161
|
+
|
|
162
|
+
# @!visibility private
|
|
163
|
+
# Chooses the Ruby accessor name for server column `field`. The natural
|
|
164
|
+
# name is `field.underscore`. It is replaced with `<name>_field` (then
|
|
165
|
+
# `<name>_field2`, ...) when it would shadow a method every
|
|
166
|
+
# Parse::Object relies on (`class`, `hash`, `save`, `send`,
|
|
167
|
+
# `object_id`, `changes`, ...), when a boolean column's class-level
|
|
168
|
+
# scope would shadow a class method (`freeze`, `name`), or when another
|
|
169
|
+
# column already claimed it (`fooBar` and `foo_bar` both underscore to
|
|
170
|
+
# `foo_bar`). The server column name is unchanged (it is kept with
|
|
171
|
+
# `field:`). Returns nil, after a warning, when no safe name exists.
|
|
172
|
+
# @return [Symbol, nil]
|
|
173
|
+
def self.safe_accessor_name(klass, field, data_type)
|
|
174
|
+
natural = field.to_s.underscore
|
|
175
|
+
unless natural.match?(/\A[a-z_][a-zA-Z0-9_]*\z/)
|
|
176
|
+
builder_warn "skipping column #{klass.parse_class}.#{field}: not a valid Ruby method name"
|
|
177
|
+
return nil
|
|
178
|
+
end
|
|
179
|
+
candidates = [natural, "#{natural}#{RENAMED_ACCESSOR_SUFFIX}"]
|
|
180
|
+
candidates.concat((2..9).map { |n| "#{natural}#{RENAMED_ACCESSOR_SUFFIX}#{n}" })
|
|
181
|
+
name = candidates.find { |c| !accessor_conflict?(klass, c.to_sym, data_type) }
|
|
182
|
+
if name.nil?
|
|
183
|
+
builder_warn "skipping column #{klass.parse_class}.#{field}: no free Ruby accessor name"
|
|
184
|
+
return nil
|
|
185
|
+
end
|
|
186
|
+
if name != natural
|
|
187
|
+
builder_warn "column #{klass.parse_class}.#{field} is exposed as ##{name} " \
|
|
188
|
+
"because ##{natural} would clash with an existing method or column"
|
|
189
|
+
end
|
|
190
|
+
name.to_sym
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# @!visibility private
|
|
194
|
+
# Whether defining accessor `key` on `klass` would replace an existing
|
|
195
|
+
# method or reuse a name another column already maps to.
|
|
196
|
+
def self.accessor_conflict?(klass, key, data_type)
|
|
197
|
+
return true if klass.fields.key?(key) || klass.field_map.key?(key)
|
|
198
|
+
return true if klass.respond_to?(:relations) && klass.relations.key?(key)
|
|
199
|
+
instance_names = [key, :"#{key}=", :"#{key}_changed?", :"#{key}_was"]
|
|
200
|
+
instance_names << :"#{key}?" if data_type == :boolean
|
|
201
|
+
return true if instance_names.any? { |m| method_taken?(klass, m) }
|
|
202
|
+
# Boolean columns also get a class-level scope named after the key.
|
|
203
|
+
return true if data_type == :boolean && klass.respond_to?(key, true)
|
|
204
|
+
false
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# @!visibility private
|
|
208
|
+
def self.method_taken?(klass, name)
|
|
209
|
+
klass.method_defined?(name) || klass.private_method_defined?(name)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# @!visibility private
|
|
213
|
+
def self.builder_warn(message)
|
|
214
|
+
warn "[Parse::Model::Builder] #{message}"
|
|
215
|
+
end
|
|
216
|
+
|
|
125
217
|
# @!visibility private
|
|
126
218
|
# Validates a server-returned `targetClass` string before forwarding
|
|
127
219
|
# it to `belongs_to`/`has_many`. Returns `nil` for missing or
|
|
@@ -34,11 +34,18 @@ module Parse
|
|
|
34
34
|
# material against snapshot/MONITOR exposure should set
|
|
35
35
|
# `PARSE_STACK_LOCK_SECRET` or `Parse.synchronize_create_secret`.
|
|
36
36
|
module CreateLock
|
|
37
|
+
# Floor for the lock lease, in seconds. When no `ttl:` is given the
|
|
38
|
+
# lease is sized from the client's request budget instead (see
|
|
39
|
+
# {.default_ttl}); this is the minimum it can be.
|
|
37
40
|
DEFAULT_TTL = 3
|
|
38
41
|
DEFAULT_WAIT = 2.0
|
|
39
42
|
DEFAULT_POLL_BASE = 0.05
|
|
40
43
|
DEFAULT_POLL_JITTER = 0.015
|
|
41
|
-
|
|
44
|
+
# Ceiling for the lock lease, in seconds. The lease only matters when a
|
|
45
|
+
# holder dies without releasing; a normal exit always releases.
|
|
46
|
+
MAX_TTL = 300
|
|
47
|
+
# Requests the locked block is sized for: the find, then the create.
|
|
48
|
+
LOCKED_REQUESTS = 2
|
|
42
49
|
MAX_WAIT = 30
|
|
43
50
|
MAX_PAYLOAD_BYTES = 8_192
|
|
44
51
|
MAX_DEPTH = 4
|
|
@@ -60,7 +67,7 @@ module Parse
|
|
|
60
67
|
def synchronize(parse_class:, query_attrs:, options: {}, session_token: nil, master_key: nil, &block)
|
|
61
68
|
raise ArgumentError, "block required" unless block_given?
|
|
62
69
|
|
|
63
|
-
ttl = clamp(Integer(options[:ttl]
|
|
70
|
+
ttl = options[:ttl] ? clamp(Integer(options[:ttl]), 1, MAX_TTL) : default_ttl
|
|
64
71
|
wait = clamp(Float(options[:wait] || DEFAULT_WAIT), 0.0, MAX_WAIT)
|
|
65
72
|
on_degraded = options[:on_degraded] || :warn
|
|
66
73
|
|
|
@@ -115,6 +122,24 @@ module Parse
|
|
|
115
122
|
end
|
|
116
123
|
end
|
|
117
124
|
|
|
125
|
+
# The lease used when no `ttl:` is given. The lock is not renewed
|
|
126
|
+
# while held, so the lease must outlast the block it guards: a find
|
|
127
|
+
# and a create, each of which may run through every retry at the full
|
|
128
|
+
# request timeout. A lease shorter than that expires mid-block, lets a
|
|
129
|
+
# second caller in, and produces the duplicate the lock exists to
|
|
130
|
+
# prevent. The result is clamped to DEFAULT_TTL..MAX_TTL. Pass `ttl:`
|
|
131
|
+
# to override it.
|
|
132
|
+
# @param client [Parse::Client, nil] the client whose timeouts and
|
|
133
|
+
# retry limit size the lease; the default client when nil.
|
|
134
|
+
# @return [Integer] seconds.
|
|
135
|
+
def default_ttl(client = nil)
|
|
136
|
+
client ||= Parse::Client.client
|
|
137
|
+
budget = client.respond_to?(:request_time_budget) ? client.request_time_budget : 0
|
|
138
|
+
clamp((budget * LOCKED_REQUESTS).ceil, DEFAULT_TTL, MAX_TTL)
|
|
139
|
+
rescue StandardError
|
|
140
|
+
DEFAULT_TTL
|
|
141
|
+
end
|
|
142
|
+
|
|
118
143
|
# Canonical lock key for the given inputs. Public for tests.
|
|
119
144
|
# @return [String]
|
|
120
145
|
def canonical_key(parse_class:, query_attrs:, session_token: nil, master_key: nil)
|
|
@@ -145,6 +145,8 @@ module Parse
|
|
|
145
145
|
unless defined?(Parse::MongoDB) && Parse::MongoDB.respond_to?(:enabled?) && Parse::MongoDB.enabled?
|
|
146
146
|
return { available: false, reason: :mongodb_not_enabled }
|
|
147
147
|
end
|
|
148
|
+
# Parse::AtlasSearch is loaded on first use, not with the SDK.
|
|
149
|
+
require_relative "../../atlas_search"
|
|
148
150
|
indexes = Parse::AtlasSearch::IndexManager.list_indexes(parse_class)
|
|
149
151
|
{
|
|
150
152
|
available: true,
|
|
@@ -55,6 +55,9 @@ module Parse
|
|
|
55
55
|
# @param preserve_changes [Boolean] if true, re-apply local dirty values to fetched fields.
|
|
56
56
|
# By default (false), fetched fields accept server values and local changes are discarded.
|
|
57
57
|
# Unfetched fields always preserve their dirty state regardless of this setting.
|
|
58
|
+
# A relation's staged additions and removals are kept in both modes (they are
|
|
59
|
+
# operations, not field values) and the relation stays marked changed, so the
|
|
60
|
+
# next save sends them. Use {Parse::Object#reload!} or `clear_changes!` to drop them.
|
|
58
61
|
# @param opts [Hash] a set of options to pass to the client request.
|
|
59
62
|
# @option opts [Boolean, Symbol] :cache (:write_only) caching mode:
|
|
60
63
|
# - :write_only (default) - skip cache read, but update cache with fresh data
|
|
@@ -87,7 +90,11 @@ module Parse
|
|
|
87
90
|
includes_array = includes.present? ? Array(includes) : nil
|
|
88
91
|
|
|
89
92
|
# Build formatted keys once (reused for query and tracking)
|
|
90
|
-
formatted_keys = keys_array
|
|
93
|
+
formatted_keys = if keys_array
|
|
94
|
+
Parse::Query.with_field_aliases(parse_class) do
|
|
95
|
+
keys_array.map { |k| Parse::Query.format_field(k) }
|
|
96
|
+
end
|
|
97
|
+
end
|
|
91
98
|
|
|
92
99
|
# Validate keys against model fields if validation is enabled
|
|
93
100
|
# Skip validation if warnings are disabled (nothing to report)
|
|
@@ -203,15 +210,26 @@ module Parse
|
|
|
203
210
|
|
|
204
211
|
# Apply attributes from server (only keys in result get updated)
|
|
205
212
|
apply_attributes!(result, dirty_track: false)
|
|
213
|
+
# The server row is now the source of truth for the ACL. A full
|
|
214
|
+
# fetch whose response has no ACL key is a public (ACL-less) row, so
|
|
215
|
+
# drop any locally stamped default rather than misreport it. Either
|
|
216
|
+
# way the record exists, so the save-time default-ACL resolver must
|
|
217
|
+
# not replace its ACL.
|
|
218
|
+
if !is_partial_fetch && !result.key?("ACL") && !result.key?(:ACL)
|
|
219
|
+
@acl = nil
|
|
220
|
+
end
|
|
221
|
+
@_acl_pristine = false
|
|
206
222
|
record_authorization_hydration!(result, partial: is_partial_fetch) if
|
|
207
223
|
respond_to?(:record_authorization_hydration!)
|
|
208
224
|
|
|
209
225
|
begin
|
|
210
|
-
clear_changes
|
|
226
|
+
# Not clear_changes!: that also drops a relation's staged
|
|
227
|
+
# additions and removals, which a fetch must keep.
|
|
228
|
+
clear_dirty_tracking!
|
|
211
229
|
rescue => e
|
|
212
230
|
# Log the error for debugging purposes
|
|
213
231
|
warn "[Parse::Fetch] Warning: clear_changes! failed: #{e.class}: #{e.message}"
|
|
214
|
-
# If
|
|
232
|
+
# If clearing fails, manually reset change tracking
|
|
215
233
|
@changed_attributes = {} if instance_variable_defined?(:@changed_attributes)
|
|
216
234
|
@mutations_from_database = nil if instance_variable_defined?(:@mutations_from_database)
|
|
217
235
|
@mutations_before_last_save = nil if instance_variable_defined?(:@mutations_before_last_save)
|
|
@@ -253,9 +271,31 @@ module Parse
|
|
|
253
271
|
end
|
|
254
272
|
end
|
|
255
273
|
|
|
274
|
+
# A relation whose proxy still holds staged additions or removals
|
|
275
|
+
# must stay dirty, or the next save sends nothing. The loop above
|
|
276
|
+
# cannot guarantee it: when the response carries the relation's
|
|
277
|
+
# descriptor, re-applying the same proxy is not a change.
|
|
278
|
+
remark_staged_relations_dirty!
|
|
279
|
+
|
|
256
280
|
self
|
|
257
281
|
end
|
|
258
282
|
|
|
283
|
+
# @!visibility private
|
|
284
|
+
# Marks dirty every relation whose proxy holds staged additions or
|
|
285
|
+
# removals. Used after a fetch has cleared the record's dirty tracking.
|
|
286
|
+
def remark_staged_relations_dirty!
|
|
287
|
+
return unless respond_to?(:relations)
|
|
288
|
+
relations.each_key do |key|
|
|
289
|
+
ivar = :"@#{key}"
|
|
290
|
+
next unless instance_variable_defined?(ivar)
|
|
291
|
+
proxy = instance_variable_get(ivar)
|
|
292
|
+
next unless proxy.respond_to?(:staged_changes?) && proxy.staged_changes?
|
|
293
|
+
next if changed.include?(key.to_s)
|
|
294
|
+
will_change_method = "#{key}_will_change!"
|
|
295
|
+
send(will_change_method) if respond_to?(will_change_method)
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
259
299
|
# Fetches the object with explicit caching enabled.
|
|
260
300
|
# This is a convenience method that calls fetch! with cache: true.
|
|
261
301
|
# Use this when you want to leverage cached responses for better performance.
|
|
@@ -318,7 +358,9 @@ module Parse
|
|
|
318
358
|
def fetch_json(keys: nil, includes: nil)
|
|
319
359
|
query = {}
|
|
320
360
|
if keys.present?
|
|
321
|
-
keys_array =
|
|
361
|
+
keys_array = Parse::Query.with_field_aliases(parse_class) do
|
|
362
|
+
Array(keys).map { |k| Parse::Query.format_field(k) }
|
|
363
|
+
end
|
|
322
364
|
query[:keys] = keys_array.join(",")
|
|
323
365
|
end
|
|
324
366
|
if includes.present?
|
|
@@ -432,9 +474,12 @@ module Parse
|
|
|
432
474
|
# Autofetch if object is a pointer OR was selectively fetched
|
|
433
475
|
# Skip if autofetch is disabled for this instance
|
|
434
476
|
needs_fetch = pointer? || has_selective_keys?
|
|
477
|
+
# A pointer never autofetches for its ACL. A partially fetched object
|
|
478
|
+
# does when the ACL was left out, so code that edits it starts from
|
|
479
|
+
# the record's real ACL instead of nil.
|
|
435
480
|
return unless needs_fetch &&
|
|
436
481
|
!autofetch_disabled? &&
|
|
437
|
-
key
|
|
482
|
+
!(key == :acl && pointer?) &&
|
|
438
483
|
!Parse::Properties::BASE_KEYS.include?(key) &&
|
|
439
484
|
respond_to?(:fetch)
|
|
440
485
|
|
|
@@ -197,20 +197,28 @@ module Parse
|
|
|
197
197
|
|
|
198
198
|
if respond_to?(:relations) && relations[field_sym]
|
|
199
199
|
proxy = public_send(field_sym)
|
|
200
|
-
#
|
|
201
|
-
# relation_change_operations
|
|
202
|
-
#
|
|
203
|
-
#
|
|
204
|
-
#
|
|
205
|
-
|
|
206
|
-
proxy.instance_variable_set(:@removals, []) if proxy.instance_variable_defined?(:@removals)
|
|
200
|
+
# Drop the pending add/remove ledger that backs
|
|
201
|
+
# relation_change_operations, and the proxy's dirty markers. The
|
|
202
|
+
# relation proxy's clear_changes! also reloads its list on next
|
|
203
|
+
# access, since the loaded list included the discarded items. A
|
|
204
|
+
# handler that reads the relation after the revert therefore
|
|
205
|
+
# triggers one query for the stored members (on a saved owner).
|
|
207
206
|
proxy.clear_changes! if proxy.respond_to?(:clear_changes!)
|
|
208
207
|
clear_attribute_changes([field_str])
|
|
209
208
|
return
|
|
210
209
|
end
|
|
211
210
|
|
|
212
211
|
if is_new
|
|
213
|
-
|
|
212
|
+
if field_sym == :acl && !self.class.builtin_acl_default_active?
|
|
213
|
+
# The class's ACL policy still decides a new record's ACL: put
|
|
214
|
+
# back the init-time default stamp and let the save-time
|
|
215
|
+
# resolver run, instead of a nil ACL that a reply would send
|
|
216
|
+
# as `{}` (master-key only).
|
|
217
|
+
public_send(:acl=, self.class.default_acls.as_json)
|
|
218
|
+
@_acl_pristine = true
|
|
219
|
+
else
|
|
220
|
+
public_send("#{field_str}=", nil)
|
|
221
|
+
end
|
|
214
222
|
else
|
|
215
223
|
restore_attributes([field_str])
|
|
216
224
|
end
|
|
@@ -4,12 +4,16 @@
|
|
|
4
4
|
module Parse
|
|
5
5
|
# Global `const_missing` hook that lazily resolves the plural form of a
|
|
6
6
|
# {Parse::Object} subclass constant to that class. Referencing `Posts`
|
|
7
|
-
# when a class `Post` exists installs `Posts` as an alias for `Post`
|
|
8
|
-
#
|
|
7
|
+
# when a class `Post` exists installs `Posts` as an alias for `Post` and
|
|
8
|
+
# returns it, so query entry points like
|
|
9
9
|
# `Posts.where(...).count` work without any per-model boilerplate.
|
|
10
10
|
#
|
|
11
|
-
# The hook is prepended onto `Module` so
|
|
12
|
-
#
|
|
11
|
+
# The hook is prepended onto `Module` so a plural reference resolves from
|
|
12
|
+
# any namespace, but the alias constant is only ever installed in the
|
|
13
|
+
# namespace that defines the singular class (`::Posts` for a top-level
|
|
14
|
+
# `Post`, `Blog::Posts` for `Blog::Post`). A lookup from an unrelated
|
|
15
|
+
# module never adds a constant to that module, and a frozen namespace is
|
|
16
|
+
# skipped rather than raising `FrozenError`. It is tightly guarded: every
|
|
13
17
|
# path that is not a plural-of-a-Parse-class falls through to `super`,
|
|
14
18
|
# preserving normal `NameError` and autoloader (Zeitwerk/classic)
|
|
15
19
|
# behavior. The whole feature is gated on {Parse.pluralized_aliases?} so
|