Module: Parse::Core::ParseReference::ClassMethods
- Defined in:
- lib/parse/model/core/parse_reference.rb
Instance Method Summary collapse
-
#parse_reference(field_name = :parse_reference, field: nil, precompute: false, index: true, unique_index: true) ⇒ Symbol
Declare a self-referential identifier field on this class.
-
#populate_parse_references!(objects) ⇒ Array<Parse::Object>
Populate the parse_reference field for an array of already-saved objects.
Instance Method Details
#parse_reference(field_name = :parse_reference, field: nil, precompute: false, index: true, unique_index: true) ⇒ Symbol
Declare a self-referential identifier field on this class. See Parse::Core::ParseReference for full documentation.
187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 |
# File 'lib/parse/model/core/parse_reference.rb', line 187 def parse_reference(field_name = :parse_reference, field: nil, precompute: false, index: true, unique_index: true) field_name = field_name.to_sym unless field_name.to_s =~ /\A[a-z_][a-z0-9_]*\z/i raise ArgumentError, "parse_reference field name must match /\\A[a-z_][a-z0-9_]*\\z/i, got #{field_name.inspect}" end remote = field || field_name.to_s.camelize(:lower) property field_name, :string, field: remote # Auto-register a MongoDB index declaration for this field. The # synchronize_create correctness floor (CHANGELOG 4.4.0) relies on # a unique index on the dedup tuple — auto-registering removes # the operator-must-remember failure mode. The index is unique # AND sparse by default: sparse so that # `Parse.populate_parse_references!` backfill can walk rows with # NULL values without tripping the unique constraint on the # second NULL. Operators can opt out per-field: # - `index: false` — skip registration entirely # - `unique_index: false` — register the index but drop the # unique constraint (cheaper lookups without the dedup guarantee) # The declaration is inert at load time; it ships through the # standard `Parse::Schema::IndexMigrator` plan/apply path so the # writer URI + triple gate still gates actual mutation. if index && respond_to?(:mongo_index) opts = { sparse: true } opts[:unique] = true if unique_index mongo_index field_name, **opts end # Auto-install read-side hiding: clients shouldn't see the # internal reference column. Master/admin reads (which is how # analytics queries and direct Mongo lookups run) are unaffected # because protect_fields("*", ...) only applies to non-master # reads. Merge into any existing "*" protected fields rather # than overwriting (the underlying set_protected_fields method # replaces by pattern). if respond_to?(:protect_fields) && respond_to?(:class_permissions) existing = .protected_fields_for("*") rescue [] merged = (existing + [field_name.to_s]).uniq protect_fields("*", merged) end # Auto-install write-side protection: once the after_create # populates the value, nothing (including master) can rewrite # it. :set_once allows the first transition from blank to a # value, then locks the field forever. if respond_to?(:guard) guard field_name, :set_once end # Define a helper that computes the canonical value and writes # via `update!` (bypassing the user's save/create callback # chain so this internal bookkeeping write doesn't double-fire # after_save hooks the user has on the class). method_name = :"_assign_#{field_name}!" define_method(method_name) do return unless id.present? target = Parse::Core::ParseReference.format(self.class.parse_class, id) return if public_send(field_name) == target public_send("#{field_name}=", target) ok = update! unless ok Parse.logger&.warn( "[Parse::ParseReference] Failed to persist #{self.class.parse_class}##{id} " \ "#{field_name} = #{target.inspect}; object exists without its reference field. " \ "errors=#{errors..inspect rescue nil}" ) end ok end # Expose the configured field name as a class-level reader so # the batch-populate helper and other introspection code can # find it without re-parsing the class body. @_parse_reference_fields ||= [] @_parse_reference_fields << field_name singleton_class.send(:attr_reader, :_parse_reference_fields) unless singleton_class.method_defined?(:_parse_reference_fields) # Register the after_create callback, but only if this exact # method isn't already in the callback chain. Re-declaration in a # subclass (or accidental double-declaration in the same class) # otherwise stacks multiple invocations and produces multiple # extra REST writes per create. The check inspects the chain by # filter name so it correctly handles both fresh registration # and inheritance from a parent that already declared. already_registered = _create_callbacks.any? do |cb| (cb.filter.to_sym rescue cb.filter) == method_name end after_create method_name unless already_registered # Belt-and-suspenders: on every save where the field's current # value diverges from the canonical "ClassName$objectId" form, # force-recompute it. This callback runs in two contexts: # # 1. Gem-side save flow — fires before `before_create`, so on a # fresh object (id blank) it's a no-op; on a subsequent # `_assign_<field>!`-triggered `update!` the value already # matches so it's also a no-op. # 2. Parse Server `beforeSave` webhook flow — Parse::Webhooks # deserializes the incoming object, runs `apply_field_guards!` # (which reverts disallowed client writes per the `:set_once` # guard above), then invokes `prepare_save!` which fires this # `:save` callback chain. The object's id has been assigned by # Parse Server at this point. If any value slipped past the # guard (master-key write, or first-write on create), this # callback overwrites it with the canonical value. The # enforcement happens server-side regardless of which SDK # originated the save. recompute_method = :"_recompute_#{field_name}!" define_method(recompute_method) do return unless id.present? target = Parse::Core::ParseReference.format(self.class.parse_class, id) return if target.nil? return if public_send(field_name) == target public_send("#{field_name}=", target) end already_recomputing = _save_callbacks.any? do |cb| cb.kind == :before && (cb.filter.to_sym rescue cb.filter) == recompute_method end before_save recompute_method unless already_recomputing if precompute precompute_method = :"_precompute_#{field_name}!" define_method(precompute_method) do # Precompute is master-key-only. Parse Server rejects a # client-supplied `objectId` in the create body unless its # `allowCustomObjectId` option is enabled, and even with that # global flag on, accepting client-set objectIds from # non-master sessions is an objectId-squatting risk # (attacker picks "admin", "root", or collides with another # tenant's id). Skip precompute when this save won't run as # master: an explicit per-save session token is present # (`with_session` / `set_session_token`), or no master key is # configured on the client at all. In those cases the legacy # after_create `_assign_<field>!` flow takes over, costing # one extra round-trip but staying within whatever # permissions the requesting session has. return if _session_token.present? return unless client.respond_to?(:master_key) && client.master_key.present? if id.blank? @id = Parse::Core::ParseReference.generate_object_id end target = Parse::Core::ParseReference.format(self.class.parse_class, id) # We just client-assigned @id, so the instance now satisfies # `pointer?` (objectId present, timestamps blank). The property # accessor's autofetch heuristic — and the setter's # prepare_for_dirty_tracking! pre-fetch — would both fire a GET # against an id Parse Server has not seen yet, producing a 101 # Object not found and aborting the create. Suppress autofetch # for the duration of this callback's writes; the actual create # POST that follows includes both objectId and parse_reference, # so server state is unaffected. was_disabled = autofetch_disabled? disable_autofetch! begin return if public_send(field_name) == target public_send("#{field_name}=", target) ensure enable_autofetch! unless was_disabled end end already_precomputing = _create_callbacks.any? do |cb| (cb.filter.to_sym rescue cb.filter) == precompute_method end # before_create runs inside Parse::Object#create, AFTER the # save dispatcher has already chosen the create-vs-update path # (actions.rb:795). Setting @id here therefore cannot reroute # the save. `new?` remains correct because it also checks # @created_at, which is still nil at this point. before_create precompute_method unless already_precomputing end field_name end |
#populate_parse_references!(objects) ⇒ Array<Parse::Object>
Populate the parse_reference field for an array of already-saved objects. Use after ‘Parse::Object.transaction` or `save_all` (both of which bypass the `:create` callback chain) so the canonical reference still lands in MongoDB. Each object gets an individual `update!` call – callers wanting tighter batching can wrap multiple updates in their own `Parse::Object.transaction`.
Objects that already have a populated reference, or that lack an objectId, are skipped silently.
385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 |
# File 'lib/parse/model/core/parse_reference.rb', line 385 def populate_parse_references!(objects) return [] if objects.nil? || objects.empty? fields_to_populate = Array(@_parse_reference_fields) return [] if fields_to_populate.empty? updated = [] objects.each do |obj| next unless obj.is_a?(self) && obj.id.present? changed_any = false fields_to_populate.each do |field_name| method = :"_assign_#{field_name}!" next unless obj.respond_to?(method) before = obj.public_send(field_name) obj.public_send(method) changed_any ||= (obj.public_send(field_name) != before) end updated << obj if changed_any end updated end |