Module: Parse::Core::ParseReference::ClassMethods

Defined in:
lib/parse/model/core/parse_reference.rb

Instance Method Summary collapse

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.

Parameters:

  • field_name (Symbol) (defaults to: :parse_reference)

    local property name (default :parse_reference)

  • field (String, nil) (defaults to: nil)

    remote Parse column name; defaults to the camelCased form of ‘field_name`

  • precompute (Boolean) (defaults to: false)

    when true, generate the objectId client-side in a ‘before_create` callback and embed the canonical reference in the initial POST body, eliminating the second round-trip. When false (default) the value is set via an `after_create` callback that issues a follow-up `update!`.

Returns:

  • (Symbol)

    the registered field name



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 = class_permissions.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.full_messages.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.

Examples:

posts = []
Post.transaction do |batch|
  3.times { posts << Post.new(title: "hi").tap { |p| batch.add(p) } }
end
Post.populate_parse_references!(posts)   # second round-trip per object

Parameters:

Returns:



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