parse-stack-next 4.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.bundle/config +2 -0
- data/.env.sample +112 -0
- data/.env.test +10 -0
- data/.github/workflows/ruby.yml +36 -0
- data/.gitignore +49 -0
- data/.ruby-version +1 -0
- data/.solargraph.yml +22 -0
- data/CHANGELOG.md +5816 -0
- data/Gemfile +30 -0
- data/Gemfile.lock +175 -0
- data/LICENSE.txt +23 -0
- data/Makefile +63 -0
- data/README.md +5655 -0
- data/Rakefile +573 -0
- data/bin/console +38 -0
- data/bin/parse-console +136 -0
- data/bin/server +17 -0
- data/bin/setup +7 -0
- data/config/parse-config.json +12 -0
- data/docs/TEST_SERVER.md +271 -0
- data/docs/_config.yml +1 -0
- data/docs/mcp_guide.md +3484 -0
- data/docs/mongodb_direct_guide.md +1348 -0
- data/docs/mongodb_index_optimization_guide.md +631 -0
- data/examples/transaction_example.rb +219 -0
- data/lib/parse/acl_scope.rb +728 -0
- data/lib/parse/agent/cancellation_token.rb +80 -0
- data/lib/parse/agent/constraint_translator.rb +480 -0
- data/lib/parse/agent/describe.rb +420 -0
- data/lib/parse/agent/errors.rb +133 -0
- data/lib/parse/agent/mcp_client.rb +557 -0
- data/lib/parse/agent/mcp_dispatcher.rb +1023 -0
- data/lib/parse/agent/mcp_rack_app.rb +1143 -0
- data/lib/parse/agent/mcp_server.rb +376 -0
- data/lib/parse/agent/metadata_audit.rb +259 -0
- data/lib/parse/agent/metadata_dsl.rb +733 -0
- data/lib/parse/agent/metadata_registry.rb +794 -0
- data/lib/parse/agent/pipeline_validator.rb +82 -0
- data/lib/parse/agent/prompts.rb +351 -0
- data/lib/parse/agent/rate_limiter.rb +158 -0
- data/lib/parse/agent/relation_graph.rb +162 -0
- data/lib/parse/agent/result_formatter.rb +453 -0
- data/lib/parse/agent/tools.rb +5489 -0
- data/lib/parse/agent.rb +3249 -0
- data/lib/parse/api/aggregate.rb +79 -0
- data/lib/parse/api/all.rb +26 -0
- data/lib/parse/api/analytics.rb +18 -0
- data/lib/parse/api/batch.rb +33 -0
- data/lib/parse/api/cloud_functions.rb +58 -0
- data/lib/parse/api/config.rb +125 -0
- data/lib/parse/api/files.rb +29 -0
- data/lib/parse/api/hooks.rb +117 -0
- data/lib/parse/api/objects.rb +146 -0
- data/lib/parse/api/path_segment.rb +75 -0
- data/lib/parse/api/push.rb +20 -0
- data/lib/parse/api/schema.rb +49 -0
- data/lib/parse/api/server.rb +50 -0
- data/lib/parse/api/sessions.rb +24 -0
- data/lib/parse/api/users.rb +250 -0
- data/lib/parse/atlas_search/index_manager.rb +353 -0
- data/lib/parse/atlas_search/result.rb +204 -0
- data/lib/parse/atlas_search/search_builder.rb +604 -0
- data/lib/parse/atlas_search/session.rb +253 -0
- data/lib/parse/atlas_search.rb +995 -0
- data/lib/parse/client/authentication.rb +97 -0
- data/lib/parse/client/batch.rb +234 -0
- data/lib/parse/client/body_builder.rb +240 -0
- data/lib/parse/client/caching.rb +203 -0
- data/lib/parse/client/logging.rb +293 -0
- data/lib/parse/client/profiling.rb +181 -0
- data/lib/parse/client/protocol.rb +91 -0
- data/lib/parse/client/request.rb +233 -0
- data/lib/parse/client/response.rb +208 -0
- data/lib/parse/client.rb +1104 -0
- data/lib/parse/clp_scope.rb +361 -0
- data/lib/parse/live_query/circuit_breaker.rb +256 -0
- data/lib/parse/live_query/client.rb +1001 -0
- data/lib/parse/live_query/configuration.rb +224 -0
- data/lib/parse/live_query/event.rb +115 -0
- data/lib/parse/live_query/event_queue.rb +272 -0
- data/lib/parse/live_query/health_monitor.rb +214 -0
- data/lib/parse/live_query/logging.rb +149 -0
- data/lib/parse/live_query/subscription.rb +294 -0
- data/lib/parse/live_query.rb +163 -0
- data/lib/parse/lookup_rewriter.rb +445 -0
- data/lib/parse/model/acl.rb +968 -0
- data/lib/parse/model/associations/belongs_to.rb +275 -0
- data/lib/parse/model/associations/collection_proxy.rb +435 -0
- data/lib/parse/model/associations/has_many.rb +597 -0
- data/lib/parse/model/associations/has_one.rb +158 -0
- data/lib/parse/model/associations/pointer_collection_proxy.rb +134 -0
- data/lib/parse/model/associations/relation_collection_proxy.rb +177 -0
- data/lib/parse/model/bytes.rb +62 -0
- data/lib/parse/model/classes/audience.rb +262 -0
- data/lib/parse/model/classes/installation.rb +363 -0
- data/lib/parse/model/classes/job_schedule.rb +153 -0
- data/lib/parse/model/classes/job_status.rb +264 -0
- data/lib/parse/model/classes/product.rb +75 -0
- data/lib/parse/model/classes/push_status.rb +263 -0
- data/lib/parse/model/classes/role.rb +751 -0
- data/lib/parse/model/classes/session.rb +201 -0
- data/lib/parse/model/classes/user.rb +943 -0
- data/lib/parse/model/clp.rb +544 -0
- data/lib/parse/model/core/actions.rb +1268 -0
- data/lib/parse/model/core/builder.rb +139 -0
- data/lib/parse/model/core/create_lock.rb +386 -0
- data/lib/parse/model/core/describe.rb +382 -0
- data/lib/parse/model/core/enhanced_change_tracking.rb +159 -0
- data/lib/parse/model/core/errors.rb +38 -0
- data/lib/parse/model/core/fetching.rb +566 -0
- data/lib/parse/model/core/field_guards.rb +220 -0
- data/lib/parse/model/core/indexing.rb +382 -0
- data/lib/parse/model/core/parse_reference.rb +407 -0
- data/lib/parse/model/core/properties.rb +809 -0
- data/lib/parse/model/core/querying.rb +491 -0
- data/lib/parse/model/core/schema.rb +202 -0
- data/lib/parse/model/core/search_indexing.rb +174 -0
- data/lib/parse/model/date.rb +88 -0
- data/lib/parse/model/email.rb +213 -0
- data/lib/parse/model/file.rb +527 -0
- data/lib/parse/model/geojson.rb +271 -0
- data/lib/parse/model/geopoint.rb +261 -0
- data/lib/parse/model/model.rb +260 -0
- data/lib/parse/model/object.rb +2068 -0
- data/lib/parse/model/phone.rb +520 -0
- data/lib/parse/model/pointer.rb +443 -0
- data/lib/parse/model/polygon.rb +406 -0
- data/lib/parse/model/push.rb +975 -0
- data/lib/parse/model/shortnames.rb +8 -0
- data/lib/parse/model/time_zone.rb +141 -0
- data/lib/parse/model/validations/uniqueness_validator.rb +97 -0
- data/lib/parse/model/validations.rb +96 -0
- data/lib/parse/mongodb.rb +2300 -0
- data/lib/parse/pipeline_security.rb +554 -0
- data/lib/parse/query/constraint.rb +198 -0
- data/lib/parse/query/constraints.rb +3279 -0
- data/lib/parse/query/cursor.rb +434 -0
- data/lib/parse/query/n_plus_one_detector.rb +445 -0
- data/lib/parse/query/operation.rb +104 -0
- data/lib/parse/query/ordering.rb +66 -0
- data/lib/parse/query.rb +7028 -0
- data/lib/parse/schema/index_migrator.rb +291 -0
- data/lib/parse/schema/search_index_migrator.rb +289 -0
- data/lib/parse/schema.rb +494 -0
- data/lib/parse/stack/generators/rails.rb +40 -0
- data/lib/parse/stack/generators/templates/model.erb +51 -0
- data/lib/parse/stack/generators/templates/model_installation.rb +4 -0
- data/lib/parse/stack/generators/templates/model_role.rb +4 -0
- data/lib/parse/stack/generators/templates/model_session.rb +4 -0
- data/lib/parse/stack/generators/templates/model_user.rb +11 -0
- data/lib/parse/stack/generators/templates/parse.rb +12 -0
- data/lib/parse/stack/generators/templates/webhooks.rb +10 -0
- data/lib/parse/stack/railtie.rb +18 -0
- data/lib/parse/stack/tasks.rb +563 -0
- data/lib/parse/stack/version.rb +11 -0
- data/lib/parse/stack.rb +455 -0
- data/lib/parse/two_factor_auth/user_extension.rb +449 -0
- data/lib/parse/two_factor_auth.rb +310 -0
- data/lib/parse/webhooks/payload.rb +360 -0
- data/lib/parse/webhooks/registration.rb +199 -0
- data/lib/parse/webhooks/replay_protection.rb +189 -0
- data/lib/parse/webhooks.rb +510 -0
- data/lib/parse-stack-next.rb +5 -0
- data/lib/parse-stack.rb +5 -0
- data/parse-stack-next.gemspec +82 -0
- data/parse-stack.png +0 -0
- data/scripts/debug-ips.js +35 -0
- data/scripts/docker/Dockerfile.parse +13 -0
- data/scripts/docker/atlas-init.js +284 -0
- data/scripts/docker/docker-compose.atlas.yml +76 -0
- data/scripts/docker/docker-compose.test.yml +106 -0
- data/scripts/docker/mongo-init.js +21 -0
- data/scripts/eval_mcp_with_lm_studio.rb +274 -0
- data/scripts/start-parse.sh +90 -0
- data/scripts/start_mcp_server.rb +78 -0
- data/scripts/test_server_connection.rb +82 -0
- metadata +377 -0
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "active_support/concern"
|
|
5
|
+
require "securerandom"
|
|
6
|
+
|
|
7
|
+
module Parse
|
|
8
|
+
module Core
|
|
9
|
+
# Declarative self-referential identifier field for Parse::Object
|
|
10
|
+
# subclasses. When `parse_reference` is declared on a class, every newly-
|
|
11
|
+
# created instance gets a string field auto-populated with the canonical
|
|
12
|
+
# `"ClassName$objectId"` form via an `after_create` callback. The value
|
|
13
|
+
# mirrors Parse Server's internal pointer-column format (`_p_team` ->
|
|
14
|
+
# `"Team$xyz"`), which makes direct MongoDB queries, `$lookup` joins, and
|
|
15
|
+
# cross-class analytics trivial: a single equality match on one column.
|
|
16
|
+
#
|
|
17
|
+
# Mechanics:
|
|
18
|
+
#
|
|
19
|
+
# * The initial `save` creates the row and returns the server-assigned
|
|
20
|
+
# objectId. An after_create callback then sets the reference field and
|
|
21
|
+
# triggers a follow-up `save` — two REST round-trips per new object.
|
|
22
|
+
# The callback is a no-op on subsequent saves once the field matches
|
|
23
|
+
# the canonical value.
|
|
24
|
+
# * The DSL is opt-in. Classes that don't call `parse_reference` get no
|
|
25
|
+
# field, no callback, and no extra writes.
|
|
26
|
+
# * The field is logically constant once set (objectId and parse_class
|
|
27
|
+
# are both immutable for the object). The DSL auto-installs three
|
|
28
|
+
# protections:
|
|
29
|
+
# 1. `protect_fields("*", [field_name])` so non-master clients never
|
|
30
|
+
# see the column on reads.
|
|
31
|
+
# 2. `guard field_name, :set_once` so once the after_create populates
|
|
32
|
+
# the field, no further write (client or master) can change it.
|
|
33
|
+
# Master-key requests do NOT bypass `:set_once` once the value is
|
|
34
|
+
# present, so a buggy migration or admin script cannot corrupt
|
|
35
|
+
# the canonical reference.
|
|
36
|
+
# 3. A `before_save` callback (`_recompute_<field_name>!`) that
|
|
37
|
+
# force-recomputes the value to `"ClassName$objectId"` whenever
|
|
38
|
+
# the field's current value diverges from the canonical form. In
|
|
39
|
+
# the Parse Server `beforeSave` webhook flow this runs after
|
|
40
|
+
# `apply_field_guards!` and corrects any spoofed value that may
|
|
41
|
+
# have come from a non-gem client (other SDK, or a direct REST
|
|
42
|
+
# POST that includes a poisoned `parseReference` on create —
|
|
43
|
+
# `:set_once` allows the first write, so this callback is the
|
|
44
|
+
# belt to that suspenders).
|
|
45
|
+
# * Inherits cleanly into `Parse::User`, `Parse::Installation`, and
|
|
46
|
+
# other system-class subclasses. The reference format becomes
|
|
47
|
+
# `"_User$objectId"`, `"_Installation$objectId"`, etc., matching
|
|
48
|
+
# Parse Server's own `_p_user`/`_p_installation` column format.
|
|
49
|
+
# * Batch / transaction caveat: `Parse::Object.transaction` and
|
|
50
|
+
# `Parse::Object.save_all` set the server-assigned objectId via
|
|
51
|
+
# `instance_variable_set` without running the `:create` callback
|
|
52
|
+
# chain. Objects created through those paths therefore do NOT have
|
|
53
|
+
# the parse_reference auto-populated. Use the
|
|
54
|
+
# {ClassMethods#populate_parse_references!} batch helper or call
|
|
55
|
+
# `obj._assign_<field>!` manually after the transaction commits.
|
|
56
|
+
#
|
|
57
|
+
# == `precompute: true` — server requirements and threat model
|
|
58
|
+
#
|
|
59
|
+
# The `precompute: true` option client-generates the objectId in a
|
|
60
|
+
# `before_create` callback and embeds both `objectId` and the canonical
|
|
61
|
+
# reference in the initial POST body, eliminating the follow-up
|
|
62
|
+
# `update!` that the default after_create flow issues. Two requirements
|
|
63
|
+
# must hold for this to work end-to-end:
|
|
64
|
+
#
|
|
65
|
+
# 1. Parse Server must be started with `allowCustomObjectId: true`
|
|
66
|
+
# (`PARSE_SERVER_ALLOW_CUSTOM_OBJECT_ID=true`). Without that flag,
|
|
67
|
+
# Parse Server rejects any create whose body contains `objectId`
|
|
68
|
+
# with `error: objectId is an invalid field name` (HTTP 400, code
|
|
69
|
+
# 105) before any cloud-code hooks run.
|
|
70
|
+
# 2. The save must run with master-key authority. The DSL enforces
|
|
71
|
+
# this SDK-side: `_precompute_<field>!` is a no-op when the
|
|
72
|
+
# instance has a per-save session token set (`with_session` /
|
|
73
|
+
# `set_session_token`) or when no `master_key` is configured on
|
|
74
|
+
# `Parse::Client`. In either case the legacy after_create
|
|
75
|
+
# `_assign_<field>!` flow takes over, costing one extra round-trip
|
|
76
|
+
# but staying within the session's permissions. The local @id
|
|
77
|
+
# falls back to the server-assigned id (no client id is generated
|
|
78
|
+
# or forwarded), so the resulting `parseReference` is correct.
|
|
79
|
+
#
|
|
80
|
+
# The SDK gate protects parse-stack callers, but `allowCustomObjectId`
|
|
81
|
+
# is a server-global flag — it also lets the JS SDK, iOS SDK, raw
|
|
82
|
+
# REST callers, and any other client using the same Parse Server pick
|
|
83
|
+
# their own `objectId` on create. That permits objectId-squatting
|
|
84
|
+
# ("admin", "root", colliding with another tenant's id), id-spoofing
|
|
85
|
+
# on classes whose ACL allows public create, and a few subtle CLP
|
|
86
|
+
# bypass shapes when a class's class-level permissions key off
|
|
87
|
+
# `objectId` patterns. To enforce master-only client objectIds across
|
|
88
|
+
# ALL SDKs, register a Cloud Code `beforeSave` hook that rejects
|
|
89
|
+
# client-supplied ids from non-master sessions, e.g.:
|
|
90
|
+
#
|
|
91
|
+
# Parse.Cloud.beforeSave("MyClass", req => {
|
|
92
|
+
# if (req.original === undefined && req.object.id && !req.master) {
|
|
93
|
+
# throw "Client-supplied objectId not allowed";
|
|
94
|
+
# }
|
|
95
|
+
# });
|
|
96
|
+
#
|
|
97
|
+
# `req.original === undefined` narrows to creates (no prior state);
|
|
98
|
+
# `req.object.id` is the client-supplied id; `!req.master` excludes
|
|
99
|
+
# legitimate master-key creates including this gem's precompute path.
|
|
100
|
+
# Apply per-class for the classes that declare
|
|
101
|
+
# `parse_reference precompute: true`, or globally on every class via
|
|
102
|
+
# `Parse.Cloud.beforeSave(Parse.Object, ...)` if the application has
|
|
103
|
+
# no legitimate non-master custom-id use case.
|
|
104
|
+
#
|
|
105
|
+
# @example default field name
|
|
106
|
+
# class Post < Parse::Object
|
|
107
|
+
# parse_reference # local :parse_reference -> remote "parseReference"
|
|
108
|
+
# end
|
|
109
|
+
# post = Post.create(title: "Hi")
|
|
110
|
+
# post.parse_reference # => "Post$abc123"
|
|
111
|
+
#
|
|
112
|
+
# @example custom local name
|
|
113
|
+
# class Event < Parse::Object
|
|
114
|
+
# parse_reference :ref
|
|
115
|
+
# end
|
|
116
|
+
#
|
|
117
|
+
# @example custom local AND remote names
|
|
118
|
+
# class Activity < Parse::Object
|
|
119
|
+
# parse_reference :ref, field: "refKey"
|
|
120
|
+
# end
|
|
121
|
+
#
|
|
122
|
+
# @example works on system class subclasses (for normal Parse::Object
|
|
123
|
+
# creates -- NOT for Parse::User#signup!, which goes through a
|
|
124
|
+
# distinct REST endpoint and does not run the `:create` callback
|
|
125
|
+
# chain. On a User subclass, populate the reference manually after
|
|
126
|
+
# signup: `user._assign_parse_reference!`.)
|
|
127
|
+
# class User < Parse::User
|
|
128
|
+
# parse_reference
|
|
129
|
+
# end
|
|
130
|
+
module ParseReference
|
|
131
|
+
extend ActiveSupport::Concern
|
|
132
|
+
|
|
133
|
+
# The separator between class name and object id. Matches Parse Server's
|
|
134
|
+
# own pointer-column format (e.g. `_p_team = "Team$abcd1234"`).
|
|
135
|
+
SEPARATOR = "$".freeze
|
|
136
|
+
|
|
137
|
+
# Length of a Parse Server objectId. Matches the format the server itself
|
|
138
|
+
# produces and what the JS/iOS SDKs generate for offline-mode local ids.
|
|
139
|
+
OBJECT_ID_LENGTH = 10
|
|
140
|
+
|
|
141
|
+
# Generate a Parse-compatible objectId: 10 characters drawn from
|
|
142
|
+
# [A-Za-z0-9]. Used by the precompute path so a `before_create` callback
|
|
143
|
+
# can assign `@id` (and the canonical reference string) before the
|
|
144
|
+
# initial POST, eliminating the second round-trip that the default
|
|
145
|
+
# after_create approach requires.
|
|
146
|
+
#
|
|
147
|
+
# 62^10 ≈ 8.39e17 keyspace; collision probability is negligible at any
|
|
148
|
+
# practical scale. Parse Server accepts client-assigned `objectId` in
|
|
149
|
+
# POST bodies (the JS/iOS SDKs use this for offline mode) and rejects
|
|
150
|
+
# duplicates with a specific error code rather than silently overwriting.
|
|
151
|
+
def self.generate_object_id
|
|
152
|
+
SecureRandom.alphanumeric(OBJECT_ID_LENGTH)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Build a canonical "Class$id" reference string. Returns nil if either
|
|
156
|
+
# piece is blank — callers wiring this into other systems can use the
|
|
157
|
+
# nil to skip writing the field.
|
|
158
|
+
def self.format(parse_class, id)
|
|
159
|
+
return nil if parse_class.to_s.empty? || id.to_s.empty?
|
|
160
|
+
"#{parse_class}#{SEPARATOR}#{id}"
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Split a "Class$id" string into [class_name, object_id]. Returns
|
|
164
|
+
# [nil, nil] for nil input; raises ArgumentError on malformed input
|
|
165
|
+
# (anything else than a string containing the separator).
|
|
166
|
+
def self.parse(string)
|
|
167
|
+
return [nil, nil] if string.nil?
|
|
168
|
+
unless string.is_a?(String) && string.include?(SEPARATOR)
|
|
169
|
+
raise ArgumentError, "not a parse_reference: #{string.inspect}"
|
|
170
|
+
end
|
|
171
|
+
string.split(SEPARATOR, 2)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
module ClassMethods
|
|
175
|
+
# Declare a self-referential identifier field on this class.
|
|
176
|
+
# See {Parse::Core::ParseReference} for full documentation.
|
|
177
|
+
#
|
|
178
|
+
# @param field_name [Symbol] local property name (default :parse_reference)
|
|
179
|
+
# @param field [String, nil] remote Parse column name; defaults to the
|
|
180
|
+
# camelCased form of `field_name`
|
|
181
|
+
# @param precompute [Boolean] when true, generate the objectId
|
|
182
|
+
# client-side in a `before_create` callback and embed the canonical
|
|
183
|
+
# reference in the initial POST body, eliminating the second
|
|
184
|
+
# round-trip. When false (default) the value is set via an
|
|
185
|
+
# `after_create` callback that issues a follow-up `update!`.
|
|
186
|
+
# @return [Symbol] the registered field name
|
|
187
|
+
def parse_reference(field_name = :parse_reference, field: nil, precompute: false,
|
|
188
|
+
index: true, unique_index: true)
|
|
189
|
+
field_name = field_name.to_sym
|
|
190
|
+
unless field_name.to_s =~ /\A[a-z_][a-z0-9_]*\z/i
|
|
191
|
+
raise ArgumentError,
|
|
192
|
+
"parse_reference field name must match /\\A[a-z_][a-z0-9_]*\\z/i, got #{field_name.inspect}"
|
|
193
|
+
end
|
|
194
|
+
remote = field || field_name.to_s.camelize(:lower)
|
|
195
|
+
property field_name, :string, field: remote
|
|
196
|
+
|
|
197
|
+
# Auto-register a MongoDB index declaration for this field. The
|
|
198
|
+
# synchronize_create correctness floor (CHANGELOG 4.4.0) relies on
|
|
199
|
+
# a unique index on the dedup tuple — auto-registering removes
|
|
200
|
+
# the operator-must-remember failure mode. The index is unique
|
|
201
|
+
# AND sparse by default: sparse so that
|
|
202
|
+
# `Parse.populate_parse_references!` backfill can walk rows with
|
|
203
|
+
# NULL values without tripping the unique constraint on the
|
|
204
|
+
# second NULL. Operators can opt out per-field:
|
|
205
|
+
# - `index: false` — skip registration entirely
|
|
206
|
+
# - `unique_index: false` — register the index but drop the
|
|
207
|
+
# unique constraint (cheaper lookups without the dedup guarantee)
|
|
208
|
+
# The declaration is inert at load time; it ships through the
|
|
209
|
+
# standard `Parse::Schema::IndexMigrator` plan/apply path so the
|
|
210
|
+
# writer URI + triple gate still gates actual mutation.
|
|
211
|
+
if index && respond_to?(:mongo_index)
|
|
212
|
+
opts = { sparse: true }
|
|
213
|
+
opts[:unique] = true if unique_index
|
|
214
|
+
mongo_index field_name, **opts
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Auto-install read-side hiding: clients shouldn't see the
|
|
218
|
+
# internal reference column. Master/admin reads (which is how
|
|
219
|
+
# analytics queries and direct Mongo lookups run) are unaffected
|
|
220
|
+
# because protect_fields("*", ...) only applies to non-master
|
|
221
|
+
# reads. Merge into any existing "*" protected fields rather
|
|
222
|
+
# than overwriting (the underlying set_protected_fields method
|
|
223
|
+
# replaces by pattern).
|
|
224
|
+
if respond_to?(:protect_fields) && respond_to?(:class_permissions)
|
|
225
|
+
existing = class_permissions.protected_fields_for("*") rescue []
|
|
226
|
+
merged = (existing + [field_name.to_s]).uniq
|
|
227
|
+
protect_fields("*", merged)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Auto-install write-side protection: once the after_create
|
|
231
|
+
# populates the value, nothing (including master) can rewrite
|
|
232
|
+
# it. :set_once allows the first transition from blank to a
|
|
233
|
+
# value, then locks the field forever.
|
|
234
|
+
if respond_to?(:guard)
|
|
235
|
+
guard field_name, :set_once
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Define a helper that computes the canonical value and writes
|
|
239
|
+
# via `update!` (bypassing the user's save/create callback
|
|
240
|
+
# chain so this internal bookkeeping write doesn't double-fire
|
|
241
|
+
# after_save hooks the user has on the class).
|
|
242
|
+
method_name = :"_assign_#{field_name}!"
|
|
243
|
+
define_method(method_name) do
|
|
244
|
+
return unless id.present?
|
|
245
|
+
target = Parse::Core::ParseReference.format(self.class.parse_class, id)
|
|
246
|
+
return if public_send(field_name) == target
|
|
247
|
+
public_send("#{field_name}=", target)
|
|
248
|
+
ok = update!
|
|
249
|
+
unless ok
|
|
250
|
+
Parse.logger&.warn(
|
|
251
|
+
"[Parse::ParseReference] Failed to persist #{self.class.parse_class}##{id} " \
|
|
252
|
+
"#{field_name} = #{target.inspect}; object exists without its reference field. " \
|
|
253
|
+
"errors=#{errors.full_messages.inspect rescue nil}"
|
|
254
|
+
)
|
|
255
|
+
end
|
|
256
|
+
ok
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Expose the configured field name as a class-level reader so
|
|
260
|
+
# the batch-populate helper and other introspection code can
|
|
261
|
+
# find it without re-parsing the class body.
|
|
262
|
+
@_parse_reference_fields ||= []
|
|
263
|
+
@_parse_reference_fields << field_name
|
|
264
|
+
singleton_class.send(:attr_reader, :_parse_reference_fields) unless singleton_class.method_defined?(:_parse_reference_fields)
|
|
265
|
+
|
|
266
|
+
# Register the after_create callback, but only if this exact
|
|
267
|
+
# method isn't already in the callback chain. Re-declaration in a
|
|
268
|
+
# subclass (or accidental double-declaration in the same class)
|
|
269
|
+
# otherwise stacks multiple invocations and produces multiple
|
|
270
|
+
# extra REST writes per create. The check inspects the chain by
|
|
271
|
+
# filter name so it correctly handles both fresh registration
|
|
272
|
+
# and inheritance from a parent that already declared.
|
|
273
|
+
already_registered = _create_callbacks.any? do |cb|
|
|
274
|
+
(cb.filter.to_sym rescue cb.filter) == method_name
|
|
275
|
+
end
|
|
276
|
+
after_create method_name unless already_registered
|
|
277
|
+
|
|
278
|
+
# Belt-and-suspenders: on every save where the field's current
|
|
279
|
+
# value diverges from the canonical "ClassName$objectId" form,
|
|
280
|
+
# force-recompute it. This callback runs in two contexts:
|
|
281
|
+
#
|
|
282
|
+
# 1. Gem-side save flow — fires before `before_create`, so on a
|
|
283
|
+
# fresh object (id blank) it's a no-op; on a subsequent
|
|
284
|
+
# `_assign_<field>!`-triggered `update!` the value already
|
|
285
|
+
# matches so it's also a no-op.
|
|
286
|
+
# 2. Parse Server `beforeSave` webhook flow — Parse::Webhooks
|
|
287
|
+
# deserializes the incoming object, runs `apply_field_guards!`
|
|
288
|
+
# (which reverts disallowed client writes per the `:set_once`
|
|
289
|
+
# guard above), then invokes `prepare_save!` which fires this
|
|
290
|
+
# `:save` callback chain. The object's id has been assigned by
|
|
291
|
+
# Parse Server at this point. If any value slipped past the
|
|
292
|
+
# guard (master-key write, or first-write on create), this
|
|
293
|
+
# callback overwrites it with the canonical value. The
|
|
294
|
+
# enforcement happens server-side regardless of which SDK
|
|
295
|
+
# originated the save.
|
|
296
|
+
recompute_method = :"_recompute_#{field_name}!"
|
|
297
|
+
define_method(recompute_method) do
|
|
298
|
+
return unless id.present?
|
|
299
|
+
target = Parse::Core::ParseReference.format(self.class.parse_class, id)
|
|
300
|
+
return if target.nil?
|
|
301
|
+
return if public_send(field_name) == target
|
|
302
|
+
public_send("#{field_name}=", target)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
already_recomputing = _save_callbacks.any? do |cb|
|
|
306
|
+
cb.kind == :before && (cb.filter.to_sym rescue cb.filter) == recompute_method
|
|
307
|
+
end
|
|
308
|
+
before_save recompute_method unless already_recomputing
|
|
309
|
+
|
|
310
|
+
if precompute
|
|
311
|
+
precompute_method = :"_precompute_#{field_name}!"
|
|
312
|
+
define_method(precompute_method) do
|
|
313
|
+
# Precompute is master-key-only. Parse Server rejects a
|
|
314
|
+
# client-supplied `objectId` in the create body unless its
|
|
315
|
+
# `allowCustomObjectId` option is enabled, and even with that
|
|
316
|
+
# global flag on, accepting client-set objectIds from
|
|
317
|
+
# non-master sessions is an objectId-squatting risk
|
|
318
|
+
# (attacker picks "admin", "root", or collides with another
|
|
319
|
+
# tenant's id). Skip precompute when this save won't run as
|
|
320
|
+
# master: an explicit per-save session token is present
|
|
321
|
+
# (`with_session` / `set_session_token`), or no master key is
|
|
322
|
+
# configured on the client at all. In those cases the legacy
|
|
323
|
+
# after_create `_assign_<field>!` flow takes over, costing
|
|
324
|
+
# one extra round-trip but staying within whatever
|
|
325
|
+
# permissions the requesting session has.
|
|
326
|
+
return if _session_token.present?
|
|
327
|
+
return unless client.respond_to?(:master_key) && client.master_key.present?
|
|
328
|
+
|
|
329
|
+
if id.blank?
|
|
330
|
+
@id = Parse::Core::ParseReference.generate_object_id
|
|
331
|
+
end
|
|
332
|
+
target = Parse::Core::ParseReference.format(self.class.parse_class, id)
|
|
333
|
+
# We just client-assigned @id, so the instance now satisfies
|
|
334
|
+
# `pointer?` (objectId present, timestamps blank). The property
|
|
335
|
+
# accessor's autofetch heuristic — and the setter's
|
|
336
|
+
# prepare_for_dirty_tracking! pre-fetch — would both fire a GET
|
|
337
|
+
# against an id Parse Server has not seen yet, producing a 101
|
|
338
|
+
# Object not found and aborting the create. Suppress autofetch
|
|
339
|
+
# for the duration of this callback's writes; the actual create
|
|
340
|
+
# POST that follows includes both objectId and parse_reference,
|
|
341
|
+
# so server state is unaffected.
|
|
342
|
+
was_disabled = autofetch_disabled?
|
|
343
|
+
disable_autofetch!
|
|
344
|
+
begin
|
|
345
|
+
return if public_send(field_name) == target
|
|
346
|
+
public_send("#{field_name}=", target)
|
|
347
|
+
ensure
|
|
348
|
+
enable_autofetch! unless was_disabled
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
already_precomputing = _create_callbacks.any? do |cb|
|
|
353
|
+
(cb.filter.to_sym rescue cb.filter) == precompute_method
|
|
354
|
+
end
|
|
355
|
+
# before_create runs inside Parse::Object#create, AFTER the
|
|
356
|
+
# save dispatcher has already chosen the create-vs-update path
|
|
357
|
+
# (actions.rb:795). Setting @id here therefore cannot reroute
|
|
358
|
+
# the save. `new?` remains correct because it also checks
|
|
359
|
+
# @created_at, which is still nil at this point.
|
|
360
|
+
before_create precompute_method unless already_precomputing
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
field_name
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# Populate the parse_reference field for an array of already-saved
|
|
367
|
+
# objects. Use after `Parse::Object.transaction` or `save_all`
|
|
368
|
+
# (both of which bypass the `:create` callback chain) so the
|
|
369
|
+
# canonical reference still lands in MongoDB. Each object gets an
|
|
370
|
+
# individual `update!` call -- callers wanting tighter batching
|
|
371
|
+
# can wrap multiple updates in their own `Parse::Object.transaction`.
|
|
372
|
+
#
|
|
373
|
+
# Objects that already have a populated reference, or that lack an
|
|
374
|
+
# objectId, are skipped silently.
|
|
375
|
+
#
|
|
376
|
+
# @example
|
|
377
|
+
# posts = []
|
|
378
|
+
# Post.transaction do |batch|
|
|
379
|
+
# 3.times { posts << Post.new(title: "hi").tap { |p| batch.add(p) } }
|
|
380
|
+
# end
|
|
381
|
+
# Post.populate_parse_references!(posts) # second round-trip per object
|
|
382
|
+
#
|
|
383
|
+
# @param objects [Array<Parse::Object>] objects to populate
|
|
384
|
+
# @return [Array<Parse::Object>] the objects that were updated
|
|
385
|
+
def populate_parse_references!(objects)
|
|
386
|
+
return [] if objects.nil? || objects.empty?
|
|
387
|
+
fields_to_populate = Array(@_parse_reference_fields)
|
|
388
|
+
return [] if fields_to_populate.empty?
|
|
389
|
+
updated = []
|
|
390
|
+
objects.each do |obj|
|
|
391
|
+
next unless obj.is_a?(self) && obj.id.present?
|
|
392
|
+
changed_any = false
|
|
393
|
+
fields_to_populate.each do |field_name|
|
|
394
|
+
method = :"_assign_#{field_name}!"
|
|
395
|
+
next unless obj.respond_to?(method)
|
|
396
|
+
before = obj.public_send(field_name)
|
|
397
|
+
obj.public_send(method)
|
|
398
|
+
changed_any ||= (obj.public_send(field_name) != before)
|
|
399
|
+
end
|
|
400
|
+
updated << obj if changed_any
|
|
401
|
+
end
|
|
402
|
+
updated
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
end
|