Module: Parse::Core::ParseReference

Extended by:
ActiveSupport::Concern
Included in:
Object
Defined in:
lib/parse/model/core/parse_reference.rb

Overview

Declarative self-referential identifier field for Parse::Object subclasses. When parse_reference is declared on a class, every newly- created instance gets a string field auto-populated with the canonical "ClassName$objectId" form via an after_create callback. The value mirrors Parse Server's internal pointer-column format (_p_workspace -> "Workspace$xyz"), which makes direct MongoDB queries, $lookup joins, and cross-class analytics trivial: a single equality match on one column.

Mechanics:

  • The initial save creates the row and returns the server-assigned objectId. An after_create callback then sets the reference field and triggers a follow-up save — two REST round-trips per new object. The callback is a no-op on subsequent saves once the field matches the canonical value.
  • The DSL is opt-in. Classes that don't call parse_reference get no field, no callback, and no extra writes.
  • The field is logically constant once set (objectId and parse_class are both immutable for the object). The DSL auto-installs three protections:
    1. protect_fields("*", [field_name]) so non-master clients never see the column on reads.
    2. guard field_name, :set_once so once the after_create populates the field, no further write (client or master) can change it. Master-key requests do NOT bypass :set_once once the value is present, so a buggy migration or admin script cannot corrupt the canonical reference.
    3. A before_save callback (_recompute_<field_name>!) that force-recomputes the value to "ClassName$objectId" whenever the field's current value diverges from the canonical form. In the Parse Server beforeSave webhook flow this runs after apply_field_guards! and corrects any spoofed value that may have come from a non-gem client (other SDK, or a direct REST POST that includes a poisoned parseReference on create — :set_once allows the first write, so this callback is the belt to that suspenders).
  • Inherits cleanly into Parse::User, Parse::Installation, and other system-class subclasses. The reference format becomes "_User$objectId", "_Installation$objectId", etc., matching Parse Server's own _p_user/_p_installation column format.
  • Batch / transaction caveat: Parse::Object.transaction and Parse::Object.save_all set the server-assigned objectId via instance_variable_set without running the :create callback chain. Objects created through those paths therefore do NOT have the parse_reference auto-populated. Use the ClassMethods#populate_parse_references! batch helper or call obj._assign_<field>! manually after the transaction commits.

precompute: true — server requirements and threat model

The precompute: true option client-generates the objectId in a before_create callback and embeds both objectId and the canonical reference in the initial POST body, eliminating the follow-up update! that the default after_create flow issues. Two requirements must hold for this to work end-to-end:

  1. Parse Server must be started with allowCustomObjectId: true (PARSE_SERVER_ALLOW_CUSTOM_OBJECT_ID=true). Without that flag, Parse Server rejects any create whose body contains objectId with error: objectId is an invalid field name (HTTP 400, code 105) before any cloud-code hooks run.
  2. The save must run with master-key authority. The DSL enforces this SDK-side: _precompute_<field>! is a no-op when the instance has a per-save session token set (with_session / set_session_token) or when no master_key is configured on Parse::Client. In either case the legacy after_create _assign_<field>! flow takes over, costing one extra round-trip but staying within the session's permissions. The local @id falls back to the server-assigned id (no client id is generated or forwarded), so the resulting parseReference is correct.

The SDK gate protects parse-stack callers, but allowCustomObjectId is a server-global flag — it also lets the JS SDK, iOS SDK, raw REST callers, and any other client using the same Parse Server pick their own objectId on create. That permits objectId-squatting ("admin", "root", colliding with another tenant's id), id-spoofing on classes whose ACL allows public create, and a few subtle CLP bypass shapes when a class's class-level permissions key off objectId patterns. To enforce master-only client objectIds across ALL SDKs, register a Cloud Code beforeSave hook that rejects client-supplied ids from non-master sessions, e.g.:

Parse.Cloud.beforeSave("MyClass", req => {
if (req.original === undefined && req.object.id && !req.master) {
  throw "Client-supplied objectId not allowed";
}
});

req.original === undefined narrows to creates (no prior state); req.object.id is the client-supplied id; !req.master excludes legitimate master-key creates including this gem's precompute path. Apply per-class for the classes that declare parse_reference precompute: true, or globally on every class via Parse.Cloud.beforeSave(Parse.Object, ...) if the application has no legitimate non-master custom-id use case.

Examples:

default field name

class Post < Parse::Object
  parse_reference   # local :parse_reference -> remote "parseReference"
end
post = Post.create(title: "Hi")
post.parse_reference   # => "Post$abc123"

custom local name

class Event < Parse::Object
  parse_reference :ref
end

custom local AND remote names

class Activity < Parse::Object
  parse_reference :ref, field: "refKey"
end

works on system class subclasses (for normal Parse::Object

creates -- NOT for Parse::User#signup!, which goes through a
distinct REST endpoint and does not run the `:create` callback
chain. On a User subclass, populate the reference manually after
signup: `user._assign_parse_reference!`.)
class User < Parse::User
  parse_reference
end

Defined Under Namespace

Modules: ClassMethods

Constant Summary collapse

SEPARATOR =

The separator between class name and object id. Matches Parse Server's own pointer-column format (e.g. _p_workspace = "Workspace$abcd1234").

"$".freeze
OBJECT_ID_LENGTH =

Length of a Parse Server objectId. Matches the format the server itself produces and what the JS/iOS SDKs generate for offline-mode local ids.

10

Class Method Summary collapse

Class Method Details

.format(parse_class, id) ⇒ Object

Build a canonical "Class$id" reference string. Returns nil if either piece is blank — callers wiring this into other systems can use the nil to skip writing the field.



158
159
160
161
# File 'lib/parse/model/core/parse_reference.rb', line 158

def self.format(parse_class, id)
  return nil if parse_class.to_s.empty? || id.to_s.empty?
  "#{parse_class}#{SEPARATOR}#{id}"
end

.generate_object_idObject

Generate a Parse-compatible objectId: 10 characters drawn from [A-Za-z0-9]. Used by the precompute path so a before_create callback can assign @id (and the canonical reference string) before the initial POST, eliminating the second round-trip that the default after_create approach requires.

62^10 ≈ 8.39e17 keyspace; collision probability is negligible at any practical scale. Parse Server accepts client-assigned objectId in POST bodies (the JS/iOS SDKs use this for offline mode) and rejects duplicates with a specific error code rather than silently overwriting.



151
152
153
# File 'lib/parse/model/core/parse_reference.rb', line 151

def self.generate_object_id
  SecureRandom.alphanumeric(OBJECT_ID_LENGTH)
end

.parse(string) ⇒ Object

Split a "Class$id" string into [class_name, object_id]. Returns [nil, nil] for nil input; raises ArgumentError on malformed input (anything else than a string containing the separator).



166
167
168
169
170
171
172
# File 'lib/parse/model/core/parse_reference.rb', line 166

def self.parse(string)
  return [nil, nil] if string.nil?
  unless string.is_a?(String) && string.include?(SEPARATOR)
    raise ArgumentError, "not a parse_reference: #{string.inspect}"
  end
  string.split(SEPARATOR, 2)
end