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:
-
‘protect_fields(“*”, [field_name])` so non-master clients never see the column on reads.
-
‘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.
-
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:
-
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.
-
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.
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
-
.format(parse_class, id) ⇒ Object
Build a canonical “Class$id” reference string.
-
.generate_object_id ⇒ Object
Generate a Parse-compatible objectId: 10 characters drawn from [A-Za-z0-9].
-
.parse(string) ⇒ Object
Split a “Class$id” string into [class_name, object_id].
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_id ⇒ Object
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 |