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_team` -> `“Team$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_team = “Team$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