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
savecreates the row and returns the server-assigned objectId. An after_create callback then sets the reference field and triggers a follow-upsave— 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_referenceget 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_onceso once the after_create populates the field, no further write (client or master) can change it. Master-key requests do NOT bypass:set_onceonce the value is present, so a buggy migration or admin script cannot corrupt the canonical reference.- A
before_savecallback (_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 ServerbeforeSavewebhook flow this runs afterapply_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 poisonedparseReferenceon create —:set_onceallows 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_installationcolumn format. - Batch / transaction caveat:
Parse::Object.transactionandParse::Object.save_allset the server-assigned objectId viainstance_variable_setwithout running the:createcallback chain. Objects created through those paths therefore do NOT have the parse_reference auto-populated. Use the ClassMethods#populate_parse_references! batch helper or callobj._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 containsobjectIdwitherror: 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 nomaster_keyis configured onParse::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 resultingparseReferenceis 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_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
-
.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 |