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,2068 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "active_model"
|
|
5
|
+
require "active_support"
|
|
6
|
+
require "active_support/inflector"
|
|
7
|
+
require "active_support/core_ext"
|
|
8
|
+
require "active_support/core_ext/object"
|
|
9
|
+
require "active_support/core_ext/string"
|
|
10
|
+
require "active_model/serializers/json"
|
|
11
|
+
require "time"
|
|
12
|
+
require "open-uri"
|
|
13
|
+
|
|
14
|
+
require_relative "../client"
|
|
15
|
+
require_relative "model"
|
|
16
|
+
require_relative "pointer"
|
|
17
|
+
require_relative "geopoint"
|
|
18
|
+
require_relative "polygon"
|
|
19
|
+
require_relative "geojson"
|
|
20
|
+
require_relative "file"
|
|
21
|
+
require_relative "bytes"
|
|
22
|
+
require_relative "date"
|
|
23
|
+
require_relative "time_zone"
|
|
24
|
+
require_relative "phone"
|
|
25
|
+
require_relative "email"
|
|
26
|
+
require_relative "acl"
|
|
27
|
+
require_relative "clp"
|
|
28
|
+
require_relative "push"
|
|
29
|
+
require_relative "core/actions"
|
|
30
|
+
require_relative "core/create_lock"
|
|
31
|
+
require_relative "core/fetching"
|
|
32
|
+
require_relative "core/querying"
|
|
33
|
+
require_relative "core/schema"
|
|
34
|
+
require_relative "core/describe"
|
|
35
|
+
require_relative "core/indexing"
|
|
36
|
+
require_relative "core/search_indexing"
|
|
37
|
+
require_relative "core/properties"
|
|
38
|
+
require_relative "core/errors"
|
|
39
|
+
require_relative "core/builder"
|
|
40
|
+
require_relative "core/enhanced_change_tracking"
|
|
41
|
+
require_relative "core/field_guards"
|
|
42
|
+
require_relative "core/parse_reference"
|
|
43
|
+
require_relative "validations"
|
|
44
|
+
require_relative "associations/has_one"
|
|
45
|
+
require_relative "associations/belongs_to"
|
|
46
|
+
require_relative "associations/has_many"
|
|
47
|
+
|
|
48
|
+
module Parse
|
|
49
|
+
# @return [Array] an array of registered Parse::Object subclasses.
|
|
50
|
+
def self.registered_classes
|
|
51
|
+
Parse::Object.descendants.map(&:parse_class).uniq
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @return [Array<Hash>] the list of all schemas for this application.
|
|
55
|
+
def self.schemas
|
|
56
|
+
client.schemas.results
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Fetch the schema for a specific collection name.
|
|
60
|
+
# @param className [String] the name collection
|
|
61
|
+
# @return [Hash] the schema document of this collection.
|
|
62
|
+
# @see Parse::Core::ClassBuilder.build!
|
|
63
|
+
def self.schema(className)
|
|
64
|
+
client.schema(className).result
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Perform a non-destructive upgrade of all your Parse schemas in the backend
|
|
68
|
+
# based on the property definitions of your local {Parse::Object} subclasses.
|
|
69
|
+
def self.auto_upgrade!
|
|
70
|
+
klassModels = Parse::Object.descendants
|
|
71
|
+
klassModels.sort_by(&:parse_class).each do |klass|
|
|
72
|
+
yield(klass) if block_given?
|
|
73
|
+
klass.auto_upgrade!
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Alias shorter names of core Parse class names.
|
|
78
|
+
# Ex, alias Parse::User to User, Parse::Installation to Installation, etc.
|
|
79
|
+
def self.use_shortnames!
|
|
80
|
+
require_relative "shortnames"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# This is the core class for all app specific Parse table subclasses. This class
|
|
84
|
+
# in herits from Parse::Pointer since an Object is a Parse::Pointer with additional fields,
|
|
85
|
+
# at a minimum, created_at, updated_at and ACLs. This class also handles all
|
|
86
|
+
# the relational types of associations in a Parse application and handles the main CRUD operations.
|
|
87
|
+
#
|
|
88
|
+
# As the parent class to all custom subclasses, this class provides the default property schema:
|
|
89
|
+
#
|
|
90
|
+
# class Parse::Object
|
|
91
|
+
# # All subclasses will inherit these properties by default.
|
|
92
|
+
#
|
|
93
|
+
# # the objectId column of a record.
|
|
94
|
+
# property :id, :string, field: :objectId
|
|
95
|
+
#
|
|
96
|
+
# # The the last updated date for a record (Parse::Date)
|
|
97
|
+
# property :updated_at, :date
|
|
98
|
+
#
|
|
99
|
+
# # The original creation date of a record (Parse::Date)
|
|
100
|
+
# property :created_at, :date
|
|
101
|
+
#
|
|
102
|
+
# # The Parse::ACL field
|
|
103
|
+
# property :acl, :acl, field: :ACL
|
|
104
|
+
#
|
|
105
|
+
# end
|
|
106
|
+
#
|
|
107
|
+
# Most Pointers and Object subclasses are treated the same. Therefore, defining a class Artist < Parse::Object
|
|
108
|
+
# that only has `id` set, will be treated as a pointer. Therefore a Parse::Object can be in a "pointer" state
|
|
109
|
+
# based on the data that it contains. Becasue of this, it is possible to take a Artist instance
|
|
110
|
+
# (in this example), that is in a pointer state, and fetch the rest of the data for that particular
|
|
111
|
+
# record without having to create a new object. Doing so would now mark it as not being a pointer anymore.
|
|
112
|
+
# This is important to the understanding on how relations and properties are handled.
|
|
113
|
+
#
|
|
114
|
+
# The implementation of this class is large and has been broken up into several modules.
|
|
115
|
+
#
|
|
116
|
+
# Properties:
|
|
117
|
+
#
|
|
118
|
+
# All columns in a Parse object are considered a type of property (ex. string, numbers, arrays, etc)
|
|
119
|
+
# except in two cases - Pointers and Relations. For a detailed discussion of properties, see
|
|
120
|
+
# The {https://github.com/modernistik/parse-stack#defining-properties Defining Properties} section.
|
|
121
|
+
#
|
|
122
|
+
# Associations:
|
|
123
|
+
#
|
|
124
|
+
# Parse supports a three main types of relational associations. One type of
|
|
125
|
+
# relation is the `One-to-One` association. This is implemented through a
|
|
126
|
+
# specific column in Parse with a Pointer data type. This pointer column,
|
|
127
|
+
# contains a local value that refers to a different record in a separate Parse
|
|
128
|
+
# table. This association is implemented using the `:belongs_to` feature. The
|
|
129
|
+
# second association is of `One-to-Many`. This is implemented is in Parse as a
|
|
130
|
+
# Array type column that contains a list of of Parse pointer objects. It is
|
|
131
|
+
# recommended by Parse that this array does not exceed 100 items for performance
|
|
132
|
+
# reasons. This feature is implemented using the `:has_many` operation with the
|
|
133
|
+
# plural name of the local Parse class. The last association type is a Parse
|
|
134
|
+
# Relation. These can be used to implement a large `Many-to-Many` association
|
|
135
|
+
# without requiring an explicit intermediary Parse table or class. This feature
|
|
136
|
+
# is also implemented using the `:has_many` method but passing the option of `:relation`.
|
|
137
|
+
#
|
|
138
|
+
# @see Associations::BelongsTo
|
|
139
|
+
# @see Associations::HasOne
|
|
140
|
+
# @see Associations::HasMany
|
|
141
|
+
class Object < Pointer
|
|
142
|
+
include Properties
|
|
143
|
+
include Core::EnhancedChangeTracking
|
|
144
|
+
include Core::FieldGuards
|
|
145
|
+
include Core::ParseReference
|
|
146
|
+
include Associations::HasOne
|
|
147
|
+
include Associations::BelongsTo
|
|
148
|
+
include Associations::HasMany
|
|
149
|
+
extend Core::Querying
|
|
150
|
+
extend Core::Schema
|
|
151
|
+
extend Core::Describe
|
|
152
|
+
extend Core::Indexing
|
|
153
|
+
extend Core::SearchIndexing
|
|
154
|
+
include Core::Fetching
|
|
155
|
+
include Core::Actions
|
|
156
|
+
# @!visibility private
|
|
157
|
+
BASE_OBJECT_CLASS = "Parse::Object".freeze
|
|
158
|
+
|
|
159
|
+
# @return [Model::TYPE_OBJECT]
|
|
160
|
+
def __type; Parse::Model::TYPE_OBJECT; end
|
|
161
|
+
|
|
162
|
+
# Default ActiveModel::Callbacks
|
|
163
|
+
# @!group Callbacks
|
|
164
|
+
#
|
|
165
|
+
# @!method before_validation
|
|
166
|
+
# A callback called before validations are run.
|
|
167
|
+
# @yield A block to execute for the callback.
|
|
168
|
+
# @see ActiveModel::Callbacks
|
|
169
|
+
# @!method after_validation
|
|
170
|
+
# A callback called after validations are run.
|
|
171
|
+
# @yield A block to execute for the callback.
|
|
172
|
+
# @see ActiveModel::Callbacks
|
|
173
|
+
# @!method around_validation
|
|
174
|
+
# A callback called around validations.
|
|
175
|
+
# @yield A block to execute for the callback.
|
|
176
|
+
# @see ActiveModel::Callbacks
|
|
177
|
+
# @!method before_create
|
|
178
|
+
# A callback called before the object has been created.
|
|
179
|
+
# @yield A block to execute for the callback.
|
|
180
|
+
# @see ActiveModel::Callbacks
|
|
181
|
+
# @!method after_create
|
|
182
|
+
# A callback called after the object has been created.
|
|
183
|
+
# @yield A block to execute for the callback.
|
|
184
|
+
# @see ActiveModel::Callbacks
|
|
185
|
+
# @!method around_create
|
|
186
|
+
# A callback called around object creation.
|
|
187
|
+
# @yield A block to execute for the callback.
|
|
188
|
+
# @see ActiveModel::Callbacks
|
|
189
|
+
# @!method before_update
|
|
190
|
+
# A callback called before the object is updated (not on create).
|
|
191
|
+
# @yield A block to execute for the callback.
|
|
192
|
+
# @see ActiveModel::Callbacks
|
|
193
|
+
# @!method after_update
|
|
194
|
+
# A callback called after the object has been updated.
|
|
195
|
+
# @yield A block to execute for the callback.
|
|
196
|
+
# @see ActiveModel::Callbacks
|
|
197
|
+
# @!method around_update
|
|
198
|
+
# A callback called around object update.
|
|
199
|
+
# @yield A block to execute for the callback.
|
|
200
|
+
# @see ActiveModel::Callbacks
|
|
201
|
+
# @!method before_save
|
|
202
|
+
# A callback called before the object is saved.
|
|
203
|
+
# @note This is not related to a Parse beforeSave webhook trigger.
|
|
204
|
+
# @yield A block to execute for the callback.
|
|
205
|
+
# @see ActiveModel::Callbacks
|
|
206
|
+
# @!method after_save
|
|
207
|
+
# A callback called after the object has been successfully saved.
|
|
208
|
+
# @note This is not related to a Parse afterSave webhook trigger.
|
|
209
|
+
# @yield A block to execute for the callback.
|
|
210
|
+
# @see ActiveModel::Callbacks
|
|
211
|
+
# @!method around_save
|
|
212
|
+
# A callback called around object save.
|
|
213
|
+
# @yield A block to execute for the callback.
|
|
214
|
+
# @see ActiveModel::Callbacks
|
|
215
|
+
# @!method before_destroy
|
|
216
|
+
# A callback called before the object is about to be deleted.
|
|
217
|
+
# @note This is not related to a Parse beforeDelete webhook trigger.
|
|
218
|
+
# @yield A block to execute for the callback.
|
|
219
|
+
# @see ActiveModel::Callbacks
|
|
220
|
+
# @!method after_destroy
|
|
221
|
+
# A callback called after the object has been successfully deleted.
|
|
222
|
+
# @note This is not related to a Parse afterDelete webhook trigger.
|
|
223
|
+
# @yield A block to execute for the callback.
|
|
224
|
+
# @see ActiveModel::Callbacks
|
|
225
|
+
# @!method around_destroy
|
|
226
|
+
# A callback called around object destruction.
|
|
227
|
+
# @yield A block to execute for the callback.
|
|
228
|
+
# @see ActiveModel::Callbacks
|
|
229
|
+
# @!endgroup
|
|
230
|
+
|
|
231
|
+
# Define all model callbacks with :before, :after, and :around support
|
|
232
|
+
# :validation - runs before/after/around validations
|
|
233
|
+
# :create - runs before/after/around creating a new object
|
|
234
|
+
# :update - runs before/after/around updating an existing object
|
|
235
|
+
# :save - runs before/after/around both create and update
|
|
236
|
+
# :destroy - runs before/after/around deleting an object
|
|
237
|
+
define_model_callbacks :validation, :create, :update, :save, :destroy, terminator: ->(target, result_lambda) { result_lambda.call == false }
|
|
238
|
+
|
|
239
|
+
# Add support for `on: :create` and `on: :update` options in validation callbacks
|
|
240
|
+
# This emulates ActiveRecord's callback behavior where you can specify:
|
|
241
|
+
# before_validation :method_name, on: :create
|
|
242
|
+
# before_validation :method_name, on: :update
|
|
243
|
+
#
|
|
244
|
+
# The `on:` option is transformed into an `if:` condition that checks new?
|
|
245
|
+
module ValidationCallbackOnSupport
|
|
246
|
+
%i[before_validation after_validation around_validation].each do |callback_method|
|
|
247
|
+
define_method(callback_method) do |*args, **options, &block|
|
|
248
|
+
# Extract the :on option and convert to :if condition
|
|
249
|
+
if options.key?(:on)
|
|
250
|
+
on_context = options.delete(:on)
|
|
251
|
+
case on_context
|
|
252
|
+
when :create
|
|
253
|
+
# Only run for new objects
|
|
254
|
+
existing_if = options[:if]
|
|
255
|
+
options[:if] = if existing_if
|
|
256
|
+
-> { new? && instance_exec(&existing_if) }
|
|
257
|
+
else
|
|
258
|
+
:new?
|
|
259
|
+
end
|
|
260
|
+
when :update
|
|
261
|
+
# Only run for existing objects
|
|
262
|
+
existing_if = options[:if]
|
|
263
|
+
options[:if] = if existing_if
|
|
264
|
+
-> { !new? && instance_exec(&existing_if) }
|
|
265
|
+
else
|
|
266
|
+
-> { !new? }
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Call the original callback method via super
|
|
272
|
+
if options.empty?
|
|
273
|
+
super(*args, &block)
|
|
274
|
+
else
|
|
275
|
+
super(*args, **options, &block)
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
singleton_class.prepend ValidationCallbackOnSupport
|
|
282
|
+
|
|
283
|
+
# Note: created_at, updated_at, and acl are defined via `property` declarations
|
|
284
|
+
# at the bottom of this file (lines ~870-878). Do not add attr_accessor here
|
|
285
|
+
# as it would be overwritten and cause "method redefined" warnings.
|
|
286
|
+
|
|
287
|
+
# All Parse Objects have a class-level and instance level `parse_class` method, in which the
|
|
288
|
+
# instance method is a convenience one for the class one. The default value for the parse_class is
|
|
289
|
+
# the name of the ruby class name. Therefore if you have an 'Artist' ruby class, then by default we will assume
|
|
290
|
+
# the remote Parse table is named 'Artist'. You may override this behavior by utilizing the `parse_class(<className>)` method
|
|
291
|
+
# to set it to something different.
|
|
292
|
+
class << self
|
|
293
|
+
attr_writer :parse_class
|
|
294
|
+
|
|
295
|
+
# @!attribute [rw] suppress_permissive_acl_warning
|
|
296
|
+
# When set on `Parse::Object` itself, suppresses the one-time per-class
|
|
297
|
+
# warning emitted when a class's effective {acl_policy_setting} is
|
|
298
|
+
# `:public` or `:owner_else_public`. Useful in test suites or apps that
|
|
299
|
+
# have deliberately reviewed and accepted permissive defaults.
|
|
300
|
+
# Defaults to `true` when `ENV["PARSE_SUPPRESS_PERMISSIVE_ACL_WARNING"]`
|
|
301
|
+
# is set to a truthy value (`1`, `true`, `yes`).
|
|
302
|
+
# @return [Boolean]
|
|
303
|
+
# @version 4.1.0
|
|
304
|
+
attr_writer :suppress_permissive_acl_warning
|
|
305
|
+
|
|
306
|
+
def suppress_permissive_acl_warning
|
|
307
|
+
return @suppress_permissive_acl_warning unless @suppress_permissive_acl_warning.nil?
|
|
308
|
+
env = ENV["PARSE_SUPPRESS_PERMISSIVE_ACL_WARNING"].to_s.downcase
|
|
309
|
+
%w[1 true yes].include?(env)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# @!attribute [rw] default_acl_private
|
|
313
|
+
# When set to true, new instances of this class will have a private ACL
|
|
314
|
+
# (no public access, master key only) instead of the default public read/write.
|
|
315
|
+
# @return [Boolean] whether new objects default to private ACLs.
|
|
316
|
+
# @version 3.1.3
|
|
317
|
+
# @example
|
|
318
|
+
# class PrivateDocument < Parse::Object
|
|
319
|
+
# self.default_acl_private = true
|
|
320
|
+
# end
|
|
321
|
+
#
|
|
322
|
+
# doc = PrivateDocument.new
|
|
323
|
+
# doc.acl.as_json # => {} (no permissions, master key only)
|
|
324
|
+
attr_accessor :default_acl_private
|
|
325
|
+
|
|
326
|
+
# Convenience method to set default ACL to private (no public access).
|
|
327
|
+
# Equivalent to `self.default_acl_private = true`.
|
|
328
|
+
# @version 3.1.3
|
|
329
|
+
# @example
|
|
330
|
+
# class PrivateDocument < Parse::Object
|
|
331
|
+
# private_acl!
|
|
332
|
+
# end
|
|
333
|
+
def private_acl!
|
|
334
|
+
self.default_acl_private = true
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# The class method to override the implicitly assumed Parse collection name
|
|
338
|
+
# in your Parse database. The default Parse collection name is the singular form
|
|
339
|
+
# of the ruby Parse::Object subclass name. The Parse class value should match to
|
|
340
|
+
# the corresponding remote table in your database in order to properly store records and
|
|
341
|
+
# perform queries.
|
|
342
|
+
# @example
|
|
343
|
+
# class Song < Parse::Object; end;
|
|
344
|
+
# class Artist < Parse::Object
|
|
345
|
+
# parse_class "Musician" # remote collection name
|
|
346
|
+
# end
|
|
347
|
+
#
|
|
348
|
+
# Parse::User.parse_class # => '_User'
|
|
349
|
+
# Song.parse_class # => 'Song'
|
|
350
|
+
# Artist.parse_class # => 'Musician'
|
|
351
|
+
#
|
|
352
|
+
# @param remoteName [String] the name of the remote collection
|
|
353
|
+
# @return [String] the name of the Parse collection for this model.
|
|
354
|
+
def parse_class(remoteName = nil)
|
|
355
|
+
@parse_class ||= model_name.name
|
|
356
|
+
@parse_class = remoteName.to_s unless remoteName.nil?
|
|
357
|
+
@parse_class
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# The set of default ACLs to be applied on newly created instances of this class.
|
|
361
|
+
# By default, public read and write are enabled unless {default_acl_private} is true.
|
|
362
|
+
# @see Parse::ACL.everyone
|
|
363
|
+
# @see Parse::ACL.private
|
|
364
|
+
# @return [Parse::ACL] the current default ACLs for this class.
|
|
365
|
+
def default_acls
|
|
366
|
+
@default_acls ||= case acl_policy_setting
|
|
367
|
+
when :public, :owner_else_public then Parse::ACL.everyone
|
|
368
|
+
when :private, :owner_else_private then Parse::ACL.private
|
|
369
|
+
else Parse::ACL.everyone
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# A method to set default ACLs to be applied for newly created
|
|
374
|
+
# instances of this class. All subclasses have public read and write enabled
|
|
375
|
+
# by default.
|
|
376
|
+
# @example
|
|
377
|
+
# class AdminData < Parse::Object
|
|
378
|
+
#
|
|
379
|
+
# # Disable public read and write
|
|
380
|
+
# set_default_acl :public, read: false, write: false
|
|
381
|
+
#
|
|
382
|
+
# # but allow members of the Admin role to read and write
|
|
383
|
+
# set_default_acl 'Admin', role: true, read: true, write: true
|
|
384
|
+
#
|
|
385
|
+
# end
|
|
386
|
+
#
|
|
387
|
+
# data = AdminData.new
|
|
388
|
+
# data.acl # => ACL({"role:Admin"=>{"read"=>true, "write"=>true}})
|
|
389
|
+
#
|
|
390
|
+
# @param id [String|:public] The name for ACL entry. This can be an objectId, a role name or :public.
|
|
391
|
+
# @param read [Boolean] Whether to allow read permissions (default: false).
|
|
392
|
+
# @param write [Boolean] Whether to allow write permissions (default: false).
|
|
393
|
+
# @param role [Boolean] Whether the `id` argument should be applied as a role name.
|
|
394
|
+
# @see Parse::ACL#apply_role
|
|
395
|
+
# @see Parse::ACL#apply
|
|
396
|
+
# @version 1.7.0
|
|
397
|
+
def set_default_acl(id, read: false, write: false, role: false)
|
|
398
|
+
unless id.present?
|
|
399
|
+
raise ArgumentError, "Invalid argument applying #{self}.default_acls : must be either objectId, role or :public"
|
|
400
|
+
end
|
|
401
|
+
# Mixing the declarative `acl_policy` DSL with the legacy additive
|
|
402
|
+
# `set_default_acl` API on the same class produces ambiguous behavior
|
|
403
|
+
# (which one wins at save time? which fields get which permissions?).
|
|
404
|
+
# Pick one and stick with it.
|
|
405
|
+
if defined?(@acl_policy_setting) && @acl_policy_setting
|
|
406
|
+
raise ArgumentError,
|
|
407
|
+
"#{self}: cannot combine `set_default_acl` with `acl_policy`. " \
|
|
408
|
+
"This class already declares `acl_policy #{@acl_policy_setting.inspect}`. " \
|
|
409
|
+
"Use the declarative DSL for the entire ACL configuration, or remove " \
|
|
410
|
+
"`acl_policy` and use only `set_default_acl` (the legacy additive API)."
|
|
411
|
+
end
|
|
412
|
+
# Mark the class as using the legacy additive ACL API. The save-time
|
|
413
|
+
# policy resolver respects this and leaves the init-stamped default
|
|
414
|
+
# ACL alone, preserving pre-4.1 behavior for classes that customize
|
|
415
|
+
# via set_default_acl.
|
|
416
|
+
@acl_default_customized_by_set_default_acl = true
|
|
417
|
+
role ? default_acls.apply_role(id, read, write) : default_acls.apply(id, read, write)
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
# @!visibility private
|
|
421
|
+
# True when {set_default_acl} has been invoked on this class. Used by
|
|
422
|
+
# the save-time policy resolver to skip classes that have opted into
|
|
423
|
+
# the legacy additive default-ACL API.
|
|
424
|
+
def acl_default_customized_by_set_default_acl?
|
|
425
|
+
defined?(@acl_default_customized_by_set_default_acl) && @acl_default_customized_by_set_default_acl
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
# @!visibility private
|
|
429
|
+
def acl(acls, owner: nil)
|
|
430
|
+
raise "[#{self}.acl DEPRECATED] - Use `#{self}.default_acl` instead."
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
# Valid ACL policies that can be passed to {acl_policy}.
|
|
434
|
+
VALID_ACL_POLICIES = [:public, :private, :owner_else_public, :owner_else_private].freeze
|
|
435
|
+
|
|
436
|
+
# Declarative ACL policy applied to newly-created instances of this class.
|
|
437
|
+
# The policy is resolved at save time so that explicit ACL changes by the
|
|
438
|
+
# caller (`obj.acl = …`, `as:` kwarg, owner-field assignment after `.new`)
|
|
439
|
+
# always take precedence over the default.
|
|
440
|
+
#
|
|
441
|
+
# Resolution order at save (only when caller has not overridden):
|
|
442
|
+
# 1. Explicit `as: user` passed at construction → owner R/W only
|
|
443
|
+
# 2. Owner pointer resolved from the declared `owner:` field → owner R/W only
|
|
444
|
+
# 3. The else-half of the policy: `:public` → public R/W, `:private` → master-key only
|
|
445
|
+
#
|
|
446
|
+
# @param policy [Symbol] one of `:public`, `:private`, `:owner_else_public`, `:owner_else_private`.
|
|
447
|
+
# @param owner [Symbol,nil] the name of the property/belongs_to whose pointer designates the owner user.
|
|
448
|
+
# Only meaningful for `:owner_else_*` policies.
|
|
449
|
+
# @example
|
|
450
|
+
# class Post < Parse::Object
|
|
451
|
+
# acl_policy :owner_else_private, owner: :author
|
|
452
|
+
# end
|
|
453
|
+
#
|
|
454
|
+
# # server-side: no owner resolvable → master-key-only fallback
|
|
455
|
+
# Post.create!(title: "draft")
|
|
456
|
+
#
|
|
457
|
+
# # owner pointer set → ACL granting R/W to that user only
|
|
458
|
+
# Post.create!(title: "live", author: current_user)
|
|
459
|
+
#
|
|
460
|
+
# # explicit caller override (works regardless of `author` field)
|
|
461
|
+
# Post.create!({ title: "x" }, as: current_user)
|
|
462
|
+
# @raise [ArgumentError] if `policy` is not one of {VALID_ACL_POLICIES}.
|
|
463
|
+
# @see VALID_ACL_POLICIES
|
|
464
|
+
# @version 4.1.0
|
|
465
|
+
def acl_policy(policy, owner: nil)
|
|
466
|
+
unless VALID_ACL_POLICIES.include?(policy)
|
|
467
|
+
raise ArgumentError, "Invalid acl_policy #{policy.inspect}; must be one of #{VALID_ACL_POLICIES.inspect}"
|
|
468
|
+
end
|
|
469
|
+
# Symmetric to the guard in set_default_acl: pick one API per class.
|
|
470
|
+
if defined?(@acl_default_customized_by_set_default_acl) && @acl_default_customized_by_set_default_acl
|
|
471
|
+
raise ArgumentError,
|
|
472
|
+
"#{self}: cannot combine `acl_policy` with `set_default_acl`. " \
|
|
473
|
+
"This class already calls `set_default_acl`. Use the declarative " \
|
|
474
|
+
"DSL for the entire ACL configuration, or remove `acl_policy` and " \
|
|
475
|
+
"use only `set_default_acl`."
|
|
476
|
+
end
|
|
477
|
+
# `owner: :self` is a special marker meaning "the record itself is
|
|
478
|
+
# its own owner" — only meaningful for Parse::User and subclasses,
|
|
479
|
+
# where the record IS a user. The save-time resolver pre-generates
|
|
480
|
+
# `@id` via Parse::Core::ParseReference.generate_object_id when
|
|
481
|
+
# blank so the ACL can grant R/W to the record's own objectId in
|
|
482
|
+
# a single roundtrip. Non-User classes have no sensible
|
|
483
|
+
# interpretation (a Post's objectId is not a user id).
|
|
484
|
+
if owner == :self && !(self <= Parse::User)
|
|
485
|
+
raise ArgumentError,
|
|
486
|
+
"#{self}: `owner: :self` is only supported on Parse::User and " \
|
|
487
|
+
"its subclasses (the record IS the owner). For other classes, " \
|
|
488
|
+
"declare a belongs_to pointer to the owning user."
|
|
489
|
+
end
|
|
490
|
+
if owner && !policy.to_s.start_with?("owner_")
|
|
491
|
+
warn "[#{self}] `owner:` is ignored when acl_policy is #{policy.inspect}; only :owner_else_public and :owner_else_private use it."
|
|
492
|
+
end
|
|
493
|
+
if owner.nil? && policy.to_s.start_with?("owner_")
|
|
494
|
+
fallback = (policy == :owner_else_public) ? "public R/W" : "master-key-only"
|
|
495
|
+
warn "[#{self}] acl_policy #{policy.inspect} declared without `owner:` field; ACL resolution will always use the fallback (#{fallback}). Pass `as:` at construction to override."
|
|
496
|
+
end
|
|
497
|
+
@acl_policy_setting = policy
|
|
498
|
+
@acl_owner_field = owner
|
|
499
|
+
# Reset materialized default_acls so it picks up the new policy's fallback half.
|
|
500
|
+
@default_acls = nil
|
|
501
|
+
# Re-arm the permissive-default warning so a subsequent change is re-evaluated.
|
|
502
|
+
@_permissive_default_warned = nil
|
|
503
|
+
policy
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
# The effective ACL policy for this class. Inherits from the superclass
|
|
507
|
+
# when not explicitly declared. The gem-wide default is
|
|
508
|
+
# `:owner_else_private` — records grant read/write to the resolved
|
|
509
|
+
# owner (from `as:` or the class's `owner:` field) when one is
|
|
510
|
+
# supplied, and fall back to master-key-only when no owner is
|
|
511
|
+
# resolvable. `default_acl_private = true` is honored as `:private`.
|
|
512
|
+
# Classes that need public access for new records should declare
|
|
513
|
+
# `acl_policy :public` or `:owner_else_public` explicitly, or use
|
|
514
|
+
# the legacy `set_default_acl` additive API.
|
|
515
|
+
# @return [Symbol] one of {VALID_ACL_POLICIES}.
|
|
516
|
+
# @version 4.1.0
|
|
517
|
+
def acl_policy_setting
|
|
518
|
+
return @acl_policy_setting if defined?(@acl_policy_setting) && @acl_policy_setting
|
|
519
|
+
return :private if default_acl_private
|
|
520
|
+
if self == Parse::Object
|
|
521
|
+
:owner_else_private
|
|
522
|
+
elsif superclass.respond_to?(:acl_policy_setting)
|
|
523
|
+
superclass.acl_policy_setting
|
|
524
|
+
else
|
|
525
|
+
:owner_else_private
|
|
526
|
+
end
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
# The name of the property/belongs_to designating the owner user for
|
|
530
|
+
# `:owner_else_*` ACL policies. Inherited from the superclass when not
|
|
531
|
+
# explicitly declared via {acl_policy}.
|
|
532
|
+
# @return [Symbol,nil]
|
|
533
|
+
# @version 4.1.0
|
|
534
|
+
def acl_owner_field
|
|
535
|
+
return @acl_owner_field if defined?(@acl_owner_field) && @acl_owner_field
|
|
536
|
+
if self != Parse::Object && superclass.respond_to?(:acl_owner_field)
|
|
537
|
+
superclass.acl_owner_field
|
|
538
|
+
else
|
|
539
|
+
nil
|
|
540
|
+
end
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
# SDK-provided Parse model class names that the policy resolver and
|
|
544
|
+
# init-time default-ACL stamp both skip. Parse Server applies its own
|
|
545
|
+
# per-class defaults for these classes when the save body omits the
|
|
546
|
+
# `ACL` field — most importantly, `_User` gets `{"<user-id>": R/W,
|
|
547
|
+
# "*": R}` so the newly created user can edit their own profile.
|
|
548
|
+
# Stamping any ACL from the SDK side (even `{}`) overrides those
|
|
549
|
+
# server-side defaults and is almost always wrong.
|
|
550
|
+
BUILTIN_PARSE_CLASS_NAMES = %w[
|
|
551
|
+
Parse::User Parse::Installation Parse::Session Parse::Role
|
|
552
|
+
Parse::Product Parse::PushStatus Parse::Audience
|
|
553
|
+
Parse::JobStatus Parse::JobSchedule
|
|
554
|
+
].freeze
|
|
555
|
+
|
|
556
|
+
# @!visibility private
|
|
557
|
+
# True when this class is one of the SDK's built-in Parse model
|
|
558
|
+
# classes ({BUILTIN_PARSE_CLASS_NAMES}). Mostly used internally to
|
|
559
|
+
# decide whether the SDK should bypass its default-ACL stamping.
|
|
560
|
+
def builtin_parse_class?
|
|
561
|
+
BUILTIN_PARSE_CLASS_NAMES.include?(name)
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
# @!visibility private
|
|
565
|
+
# True when this class is a built-in AND the application has not
|
|
566
|
+
# customized its ACL configuration via either `acl_policy` or
|
|
567
|
+
# `set_default_acl`. Under these conditions the SDK leaves `obj.acl`
|
|
568
|
+
# nil so the save body omits the `ACL` field and Parse Server applies
|
|
569
|
+
# its own per-class defaults (most importantly, `_User` → self R/W +
|
|
570
|
+
# public read). If the application has called `acl_policy` or
|
|
571
|
+
# `set_default_acl` on the built-in, the SDK respects that
|
|
572
|
+
# customization and runs the normal stamp / resolver path.
|
|
573
|
+
def builtin_acl_default_active?
|
|
574
|
+
return false unless builtin_parse_class?
|
|
575
|
+
return false if defined?(@acl_policy_setting) && @acl_policy_setting
|
|
576
|
+
return false if defined?(@acl_default_customized_by_set_default_acl) &&
|
|
577
|
+
@acl_default_customized_by_set_default_acl
|
|
578
|
+
true
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
# @!visibility private
|
|
582
|
+
# Emits a one-time warning per class when the effective default ACL policy
|
|
583
|
+
# is permissive (`:public` or `:owner_else_public`). Suppressed for the
|
|
584
|
+
# Parse::Object base class and the SDK's built-in Parse model classes.
|
|
585
|
+
# Set `Parse::Object.suppress_permissive_acl_warning = true` globally (or
|
|
586
|
+
# via the `PARSE_SUPPRESS_PERMISSIVE_ACL_WARNING` env var) to disable.
|
|
587
|
+
def _warn_permissive_acl_default_once
|
|
588
|
+
return if defined?(@_permissive_default_warned) && @_permissive_default_warned
|
|
589
|
+
@_permissive_default_warned = true
|
|
590
|
+
return if self == Parse::Object
|
|
591
|
+
return if BUILTIN_PARSE_CLASS_NAMES.include?(name)
|
|
592
|
+
return if Parse::Object.suppress_permissive_acl_warning
|
|
593
|
+
policy = acl_policy_setting
|
|
594
|
+
return unless policy == :public || policy == :owner_else_public
|
|
595
|
+
warn "[Parse::Stack security] #{self} uses permissive default ACL policy " \
|
|
596
|
+
"`#{policy}`. New records can be modified by anyone unless an owner " \
|
|
597
|
+
"is resolved at save. Call `acl_policy :owner_else_private` or " \
|
|
598
|
+
"`:private` in the class to silence this warning."
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
# @!group Class-Level Permissions (CLP)
|
|
602
|
+
|
|
603
|
+
# The Class-Level Permissions for this model.
|
|
604
|
+
# CLPs control access to the class at the schema level.
|
|
605
|
+
# @return [Parse::CLP] the CLP instance for this class
|
|
606
|
+
# @see Parse::CLP
|
|
607
|
+
def class_permissions
|
|
608
|
+
@class_permissions ||= Parse::CLP.new
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
alias_method :clp, :class_permissions
|
|
612
|
+
|
|
613
|
+
# Set default permissions for all CLP operations at once.
|
|
614
|
+
# This is useful for establishing a baseline before customizing specific operations.
|
|
615
|
+
#
|
|
616
|
+
# @param public [Boolean] whether public access is allowed for all operations
|
|
617
|
+
# @param roles [Array<String>] role names that have access to all operations
|
|
618
|
+
# @param requires_authentication [Boolean] whether authentication is required for all operations
|
|
619
|
+
#
|
|
620
|
+
# @example Public read, authenticated write
|
|
621
|
+
# class Document < Parse::Object
|
|
622
|
+
# # Start with public read access for all operations
|
|
623
|
+
# set_default_clp public: true
|
|
624
|
+
#
|
|
625
|
+
# # Then restrict write operations
|
|
626
|
+
# set_clp :create, requires_authentication: true
|
|
627
|
+
# set_clp :update, requires_authentication: true
|
|
628
|
+
# set_clp :delete, public: false, roles: ["Admin"]
|
|
629
|
+
# end
|
|
630
|
+
#
|
|
631
|
+
# @example Role-based access for everything
|
|
632
|
+
# class AdminReport < Parse::Object
|
|
633
|
+
# # Only admins can do anything
|
|
634
|
+
# set_default_clp public: false, roles: ["Admin"]
|
|
635
|
+
# end
|
|
636
|
+
#
|
|
637
|
+
# @example Authenticated users only
|
|
638
|
+
# class PrivateData < Parse::Object
|
|
639
|
+
# # Require authentication for all operations
|
|
640
|
+
# set_default_clp requires_authentication: true
|
|
641
|
+
# end
|
|
642
|
+
def set_default_clp(public: nil, roles: [], requires_authentication: false)
|
|
643
|
+
# Set the default permission on the CLP instance
|
|
644
|
+
# This will be used by as_json to fill in missing operations
|
|
645
|
+
class_permissions.set_default_permission(
|
|
646
|
+
public_access: public,
|
|
647
|
+
roles: Array(roles),
|
|
648
|
+
requires_authentication: requires_authentication
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
# Also explicitly set all operations to ensure they're included
|
|
652
|
+
Parse::CLP::OPERATIONS.each do |operation|
|
|
653
|
+
set_clp(operation, public: public, roles: roles, requires_authentication: requires_authentication)
|
|
654
|
+
end
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
# Set pointer-permission fields for read access.
|
|
658
|
+
# Users pointed to by these fields can read objects of this class.
|
|
659
|
+
# This is an alternative to ACLs for owner-based access control.
|
|
660
|
+
#
|
|
661
|
+
# @param fields [Array<Symbol, String>] pointer field names (snake_case supported)
|
|
662
|
+
# @example
|
|
663
|
+
# class Document < Parse::Object
|
|
664
|
+
# belongs_to :owner, as: :user
|
|
665
|
+
# belongs_to :editor, as: :user
|
|
666
|
+
#
|
|
667
|
+
# # Only owner and editor can read
|
|
668
|
+
# set_read_user_fields :owner, :editor
|
|
669
|
+
# end
|
|
670
|
+
def set_read_user_fields(*fields)
|
|
671
|
+
converted = fields.flatten.map do |f|
|
|
672
|
+
field_sym = f.to_sym
|
|
673
|
+
field_map[field_sym] || f.to_s.camelize(:lower)
|
|
674
|
+
end
|
|
675
|
+
class_permissions.set_read_user_fields(*converted)
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
# Set pointer-permission fields for write access.
|
|
679
|
+
# Users pointed to by these fields can write to objects of this class.
|
|
680
|
+
#
|
|
681
|
+
# @param fields [Array<Symbol, String>] pointer field names (snake_case supported)
|
|
682
|
+
# @example
|
|
683
|
+
# class Document < Parse::Object
|
|
684
|
+
# belongs_to :owner, as: :user
|
|
685
|
+
#
|
|
686
|
+
# # Only owner can write
|
|
687
|
+
# set_write_user_fields :owner
|
|
688
|
+
# end
|
|
689
|
+
def set_write_user_fields(*fields)
|
|
690
|
+
converted = fields.flatten.map do |f|
|
|
691
|
+
field_sym = f.to_sym
|
|
692
|
+
field_map[field_sym] || f.to_s.camelize(:lower)
|
|
693
|
+
end
|
|
694
|
+
class_permissions.set_write_user_fields(*converted)
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
# Set a class-level permission for a specific operation.
|
|
698
|
+
# This is the main DSL method for configuring CLPs in your model.
|
|
699
|
+
#
|
|
700
|
+
# @param operation [Symbol] the operation (:find, :get, :count, :create, :update, :delete, :addField)
|
|
701
|
+
# @param public [Boolean, nil] whether public access is allowed
|
|
702
|
+
# @param roles [Array<String>, String] role names that have access
|
|
703
|
+
# @param users [Array<String>, String] user objectIds that have access
|
|
704
|
+
# @param pointer_fields [Array<String>, String] pointer field names for userField access
|
|
705
|
+
# @param requires_authentication [Boolean] whether authentication is required
|
|
706
|
+
#
|
|
707
|
+
# @example Basic usage
|
|
708
|
+
# class Song < Parse::Object
|
|
709
|
+
# # Allow public read
|
|
710
|
+
# set_clp :find, public: true
|
|
711
|
+
# set_clp :get, public: true
|
|
712
|
+
#
|
|
713
|
+
# # Restrict write operations to specific roles
|
|
714
|
+
# set_clp :create, public: false, roles: ["Admin", "Editor"]
|
|
715
|
+
# set_clp :update, public: false, roles: ["Admin", "Editor"]
|
|
716
|
+
# set_clp :delete, public: false, roles: ["Admin"]
|
|
717
|
+
# end
|
|
718
|
+
#
|
|
719
|
+
# @example Requiring authentication
|
|
720
|
+
# class PrivateData < Parse::Object
|
|
721
|
+
# set_clp :find, requires_authentication: true
|
|
722
|
+
# set_clp :get, requires_authentication: true
|
|
723
|
+
# end
|
|
724
|
+
#
|
|
725
|
+
# @see Parse::CLP#set_permission
|
|
726
|
+
def set_clp(operation, public: nil, roles: [], users: [], pointer_fields: [], requires_authentication: false)
|
|
727
|
+
# Convert snake_case pointer field names to camelCase
|
|
728
|
+
converted_pointer_fields = Array(pointer_fields).map do |field|
|
|
729
|
+
field_sym = field.to_sym
|
|
730
|
+
field_map[field_sym] || field.to_s.camelize(:lower)
|
|
731
|
+
end
|
|
732
|
+
|
|
733
|
+
class_permissions.set_permission(
|
|
734
|
+
operation,
|
|
735
|
+
public_access: public,
|
|
736
|
+
roles: Array(roles),
|
|
737
|
+
users: Array(users),
|
|
738
|
+
pointer_fields: converted_pointer_fields,
|
|
739
|
+
requires_authentication: requires_authentication
|
|
740
|
+
)
|
|
741
|
+
end
|
|
742
|
+
|
|
743
|
+
alias_method :set_class_permission, :set_clp
|
|
744
|
+
|
|
745
|
+
# Lock every CLP operation to master-key access only. Use as a starting
|
|
746
|
+
# point when a class should be entirely hidden from clients; you can
|
|
747
|
+
# then selectively open specific operations with {set_clp} or
|
|
748
|
+
# {set_class_access} afterward.
|
|
749
|
+
#
|
|
750
|
+
# @example Hide a class entirely from clients
|
|
751
|
+
# class AuditLog < Parse::Object
|
|
752
|
+
# master_only_class!
|
|
753
|
+
# end
|
|
754
|
+
#
|
|
755
|
+
# @example Hide everything, then open create+get for clients
|
|
756
|
+
# class Invitation < Parse::Object
|
|
757
|
+
# master_only_class!
|
|
758
|
+
# set_clp :create, public: true
|
|
759
|
+
# set_clp :get, public: true
|
|
760
|
+
# end
|
|
761
|
+
#
|
|
762
|
+
# @return [void]
|
|
763
|
+
def master_only_class!
|
|
764
|
+
Parse::CLP::OPERATIONS.each { |op| set_clp(op) }
|
|
765
|
+
nil
|
|
766
|
+
end
|
|
767
|
+
|
|
768
|
+
# Restrict `find` and `count` to master-key only, leaving the other
|
|
769
|
+
# operations (`get`, `create`, `update`, `delete`, `addField`) at their
|
|
770
|
+
# current settings. This is the canonical "Installation-style" pattern:
|
|
771
|
+
# clients can interact with individual records but cannot enumerate or
|
|
772
|
+
# count them.
|
|
773
|
+
#
|
|
774
|
+
# @example Mirror _Installation semantics
|
|
775
|
+
# class Invitation < Parse::Object
|
|
776
|
+
# unlistable_class!
|
|
777
|
+
# # clients can still get/create/update/delete by objectId
|
|
778
|
+
# end
|
|
779
|
+
#
|
|
780
|
+
# @return [void]
|
|
781
|
+
def unlistable_class!
|
|
782
|
+
set_clp(:find)
|
|
783
|
+
set_clp(:count)
|
|
784
|
+
nil
|
|
785
|
+
end
|
|
786
|
+
|
|
787
|
+
# Set CLP for multiple operations in one call, choosing a coarse access
|
|
788
|
+
# mode per operation. Each value can be:
|
|
789
|
+
#
|
|
790
|
+
# * `:master` / `:master_only` / `nil` / `false` -- master key only
|
|
791
|
+
# (Parse Server's empty `{}` permission for that op)
|
|
792
|
+
# * `:public` / `true` -- wildcard `*` access
|
|
793
|
+
# * `:authenticated` -- requiresAuthentication
|
|
794
|
+
# * a String or Symbol -- a single role name
|
|
795
|
+
# (the `role:` prefix is added automatically)
|
|
796
|
+
# * an Array of Strings/Symbols -- multiple role names
|
|
797
|
+
#
|
|
798
|
+
# Operations not listed in the hash are left at their current setting.
|
|
799
|
+
# For finer control (mixed roles, users, pointer-fields,
|
|
800
|
+
# requires_authentication) use {set_clp} directly.
|
|
801
|
+
#
|
|
802
|
+
# @example The _Installation pattern -- get-by-id and create, but no listing
|
|
803
|
+
# class Invitation < Parse::Object
|
|
804
|
+
# set_class_access(
|
|
805
|
+
# find: :master, # nobody can list
|
|
806
|
+
# count: :master, # nobody can count
|
|
807
|
+
# get: :public, # anyone with the id can fetch
|
|
808
|
+
# create: :authenticated, # logged-in users may create
|
|
809
|
+
# update: :master, # only server may update
|
|
810
|
+
# delete: :master, # only server may delete
|
|
811
|
+
# )
|
|
812
|
+
# end
|
|
813
|
+
#
|
|
814
|
+
# @example Admin-only writes, public reads
|
|
815
|
+
# class Article < Parse::Object
|
|
816
|
+
# set_class_access(
|
|
817
|
+
# find: :public, get: :public,
|
|
818
|
+
# create: "Admin", update: "Admin", delete: "Admin",
|
|
819
|
+
# )
|
|
820
|
+
# end
|
|
821
|
+
#
|
|
822
|
+
# @param ops_to_access [Hash{Symbol => Symbol,String,Array,Boolean,nil}]
|
|
823
|
+
# @return [void]
|
|
824
|
+
def set_class_access(**ops_to_access)
|
|
825
|
+
ops_to_access.each do |op, access|
|
|
826
|
+
op = op.to_sym
|
|
827
|
+
unless Parse::CLP::OPERATIONS.include?(op)
|
|
828
|
+
raise ArgumentError,
|
|
829
|
+
"Unknown CLP operation #{op.inspect}. Allowed: #{Parse::CLP::OPERATIONS.inspect}"
|
|
830
|
+
end
|
|
831
|
+
case access
|
|
832
|
+
when :master, :master_only, nil, false
|
|
833
|
+
set_clp(op)
|
|
834
|
+
when :public, true
|
|
835
|
+
set_clp(op, public: true)
|
|
836
|
+
when :authenticated
|
|
837
|
+
set_clp(op, requires_authentication: true)
|
|
838
|
+
when Array
|
|
839
|
+
set_clp(op, roles: access.map(&:to_s))
|
|
840
|
+
when String, Symbol
|
|
841
|
+
set_clp(op, roles: [access.to_s])
|
|
842
|
+
else
|
|
843
|
+
raise ArgumentError,
|
|
844
|
+
"Unknown class_access value for :#{op}: #{access.inspect}. " \
|
|
845
|
+
"Use :master, :public, :authenticated, a role name, or an array of roles."
|
|
846
|
+
end
|
|
847
|
+
end
|
|
848
|
+
nil
|
|
849
|
+
end
|
|
850
|
+
|
|
851
|
+
# Define protected fields that should be hidden from certain users/roles.
|
|
852
|
+
# This is used to implement field-level security.
|
|
853
|
+
#
|
|
854
|
+
# Field names are automatically converted from snake_case (Ruby convention)
|
|
855
|
+
# to camelCase (Parse Server convention). You can use either format.
|
|
856
|
+
#
|
|
857
|
+
# @param pattern [String, Symbol] the pattern to apply protection for:
|
|
858
|
+
# - "*" or :public - applies to all users (public)
|
|
859
|
+
# - "role:RoleName" - applies to users in a specific role
|
|
860
|
+
# - "userField:fieldName" - applies to users referenced in a pointer field
|
|
861
|
+
# - user objectId - applies to a specific user
|
|
862
|
+
# @param fields [Array<String, Symbol>] field names to hide from this pattern.
|
|
863
|
+
# Use Ruby property names (snake_case) - they will be auto-converted.
|
|
864
|
+
# An empty array means the user can see all fields.
|
|
865
|
+
#
|
|
866
|
+
# @example Hide fields from public but allow admins to see everything
|
|
867
|
+
# class User < Parse::Object
|
|
868
|
+
# property :email, :string
|
|
869
|
+
# property :phone, :string
|
|
870
|
+
# property :internal_notes, :string
|
|
871
|
+
#
|
|
872
|
+
# # Hide sensitive fields from public (use snake_case Ruby names)
|
|
873
|
+
# protect_fields "*", [:email, :phone, :internal_notes]
|
|
874
|
+
#
|
|
875
|
+
# # Admins can see everything (empty array = no restrictions)
|
|
876
|
+
# protect_fields "role:Admin", []
|
|
877
|
+
#
|
|
878
|
+
# # Users can see their own data
|
|
879
|
+
# protect_fields "userField:objectId", []
|
|
880
|
+
# end
|
|
881
|
+
#
|
|
882
|
+
# @example Hide metadata from non-owners
|
|
883
|
+
# class Image < Parse::Object
|
|
884
|
+
# property :url, :string
|
|
885
|
+
# property :metadata, :object # GPS, camera info, etc.
|
|
886
|
+
# belongs_to :owner, as: :user
|
|
887
|
+
#
|
|
888
|
+
# # Hide metadata from everyone (auto-converts to "metadata" in Parse)
|
|
889
|
+
# protect_fields "*", [:metadata]
|
|
890
|
+
#
|
|
891
|
+
# # But owners can see their own image metadata
|
|
892
|
+
# protect_fields "userField:owner", []
|
|
893
|
+
# end
|
|
894
|
+
#
|
|
895
|
+
# @example Master key only fields
|
|
896
|
+
# class SensitiveDoc < Parse::Object
|
|
897
|
+
# property :admin_notes, :string
|
|
898
|
+
# property :internal_score, :integer
|
|
899
|
+
#
|
|
900
|
+
# # Only master key can see these fields
|
|
901
|
+
# # (converts to ["adminNotes", "internalScore"] for Parse Server)
|
|
902
|
+
# protect_fields "*", [:admin_notes, :internal_score]
|
|
903
|
+
# end
|
|
904
|
+
#
|
|
905
|
+
# @see Parse::CLP#set_protected_fields
|
|
906
|
+
def protect_fields(pattern, fields)
|
|
907
|
+
pattern = "*" if pattern.to_sym == :public rescue pattern
|
|
908
|
+
|
|
909
|
+
# Convert userField:field_name pattern to use camelCase field name
|
|
910
|
+
if pattern.to_s.start_with?("userField:")
|
|
911
|
+
field_name = pattern.to_s.sub("userField:", "")
|
|
912
|
+
field_sym = field_name.to_sym
|
|
913
|
+
converted_field = field_map[field_sym] || field_name.camelize(:lower)
|
|
914
|
+
pattern = "userField:#{converted_field}"
|
|
915
|
+
end
|
|
916
|
+
|
|
917
|
+
# Convert snake_case Ruby property names to camelCase Parse field names
|
|
918
|
+
converted_fields = Array(fields).map do |field|
|
|
919
|
+
field_sym = field.to_sym
|
|
920
|
+
# Use field_map if available, otherwise convert to camelCase
|
|
921
|
+
field_map[field_sym] || field.to_s.camelize(:lower)
|
|
922
|
+
end
|
|
923
|
+
class_permissions.set_protected_fields(pattern, converted_fields)
|
|
924
|
+
end
|
|
925
|
+
|
|
926
|
+
alias_method :set_protected_fields, :protect_fields
|
|
927
|
+
|
|
928
|
+
# Introspect the locally-configured access surface for this class.
|
|
929
|
+
# Combines the CLP operations, protectedFields read-side hiding, and
|
|
930
|
+
# the write-side protections installed via the field_guards DSL into
|
|
931
|
+
# a single hash, so it's easy to audit who can do what to which
|
|
932
|
+
# fields without reading three separate parts of the class body.
|
|
933
|
+
#
|
|
934
|
+
# The hash is built from the Parse-Stack model declarations only. It
|
|
935
|
+
# does NOT round-trip the Parse Server schema; if you've configured
|
|
936
|
+
# CLPs on the server side that haven't been mirrored locally, those
|
|
937
|
+
# won't appear here. Conversely, calling `update_clp!` pushes what
|
|
938
|
+
# this method reflects.
|
|
939
|
+
#
|
|
940
|
+
# @example
|
|
941
|
+
# class Post < Parse::Object
|
|
942
|
+
# property :title, :string
|
|
943
|
+
# property :owner, :string
|
|
944
|
+
# guard :owner, :master_only
|
|
945
|
+
# parse_reference
|
|
946
|
+
# set_class_access(find: :public, create: :authenticated, update: "Admin")
|
|
947
|
+
# end
|
|
948
|
+
#
|
|
949
|
+
# Post.describe_access
|
|
950
|
+
# # =>
|
|
951
|
+
# # {
|
|
952
|
+
# # operations: {
|
|
953
|
+
# # find: { "*" => true },
|
|
954
|
+
# # create: { "requiresAuthentication" => true },
|
|
955
|
+
# # update: { "role:Admin" => true },
|
|
956
|
+
# # ...
|
|
957
|
+
# # },
|
|
958
|
+
# # read_user_fields: [],
|
|
959
|
+
# # write_user_fields: [],
|
|
960
|
+
# # fields: {
|
|
961
|
+
# # title: { write: :open, read: :open },
|
|
962
|
+
# # owner: { write: :master_only, read: :open },
|
|
963
|
+
# # parse_reference: { write: :set_once, read: { hidden_from: ["*"] } },
|
|
964
|
+
# # },
|
|
965
|
+
# # }
|
|
966
|
+
#
|
|
967
|
+
# @return [Hash]
|
|
968
|
+
def describe_access
|
|
969
|
+
perms = class_permissions
|
|
970
|
+
protected_by_pattern = perms.respond_to?(:protected_fields) ? perms.protected_fields : {}
|
|
971
|
+
guards_map = respond_to?(:field_guards) && field_guards ? field_guards : {}
|
|
972
|
+
|
|
973
|
+
# Per-field access summary. Iterate `field_map` (local -> remote)
|
|
974
|
+
# rather than `fields`, because `fields` redundantly stores BOTH
|
|
975
|
+
# the local key (e.g. :full_name) and the remote key (:fullName)
|
|
976
|
+
# for every property. That redundancy would cause multi-word
|
|
977
|
+
# properties to appear twice in the output.
|
|
978
|
+
per_field = {}
|
|
979
|
+
field_map.each do |local_sym, remote_sym|
|
|
980
|
+
local_sym = local_sym.to_sym
|
|
981
|
+
next if Parse::Properties::CORE_FIELDS.key?(local_sym)
|
|
982
|
+
data_type = fields[local_sym]
|
|
983
|
+
remote = remote_sym.to_s
|
|
984
|
+
|
|
985
|
+
# Read protection -- collect every protectedFields pattern that
|
|
986
|
+
# lists this field (under either its local or remote name).
|
|
987
|
+
hidden_from = protected_by_pattern.each_with_object([]) do |(pattern, hidden_fields), acc|
|
|
988
|
+
acc << pattern if hidden_fields.include?(remote) || hidden_fields.include?(local_sym.to_s)
|
|
989
|
+
end
|
|
990
|
+
|
|
991
|
+
per_field[local_sym] = {
|
|
992
|
+
write: guards_map[local_sym] || :open,
|
|
993
|
+
read: hidden_from.empty? ? :open : { hidden_from: hidden_from },
|
|
994
|
+
type: data_type,
|
|
995
|
+
}
|
|
996
|
+
end
|
|
997
|
+
|
|
998
|
+
# Deep-copy the operations hash so callers mutating the result
|
|
999
|
+
# don't accidentally mutate the live class_permissions state.
|
|
1000
|
+
operations = if perms.respond_to?(:permissions)
|
|
1001
|
+
perms.permissions.transform_values { |v| v.is_a?(Hash) ? v.dup : v }
|
|
1002
|
+
else
|
|
1003
|
+
{}
|
|
1004
|
+
end
|
|
1005
|
+
|
|
1006
|
+
{
|
|
1007
|
+
operations: operations,
|
|
1008
|
+
read_user_fields: perms.respond_to?(:read_user_fields) ? perms.read_user_fields : [],
|
|
1009
|
+
write_user_fields: perms.respond_to?(:write_user_fields) ? perms.write_user_fields : [],
|
|
1010
|
+
fields: per_field,
|
|
1011
|
+
}
|
|
1012
|
+
end
|
|
1013
|
+
|
|
1014
|
+
# Fetch the current CLP from the Parse Server for this class.
|
|
1015
|
+
# @param client [Parse::Client] optional client to use
|
|
1016
|
+
# @return [Parse::CLP] the CLP from the server
|
|
1017
|
+
def fetch_clp(client: nil)
|
|
1018
|
+
client ||= self.client
|
|
1019
|
+
response = client.schema(parse_class)
|
|
1020
|
+
return Parse::CLP.new unless response.success?
|
|
1021
|
+
|
|
1022
|
+
clp_data = response.result["classLevelPermissions"] || {}
|
|
1023
|
+
Parse::CLP.new(clp_data)
|
|
1024
|
+
end
|
|
1025
|
+
|
|
1026
|
+
alias_method :fetch_class_permissions, :fetch_clp
|
|
1027
|
+
|
|
1028
|
+
# Update the CLP on the Parse Server for this class.
|
|
1029
|
+
# Merges local CLP with any existing server CLP.
|
|
1030
|
+
#
|
|
1031
|
+
# @param client [Parse::Client] optional client to use
|
|
1032
|
+
# @param replace [Boolean] if true, replaces server CLP entirely; otherwise merges
|
|
1033
|
+
# @return [Parse::Response] the response from the server
|
|
1034
|
+
#
|
|
1035
|
+
# @example Push local CLP to server
|
|
1036
|
+
# Song.update_clp!
|
|
1037
|
+
#
|
|
1038
|
+
# @example Replace server CLP entirely
|
|
1039
|
+
# Song.update_clp!(replace: true)
|
|
1040
|
+
def update_clp!(client: nil, replace: false)
|
|
1041
|
+
client ||= self.client
|
|
1042
|
+
|
|
1043
|
+
unless client.master_key.present?
|
|
1044
|
+
warn "[Parse] CLP changes for #{parse_class} require the master key!"
|
|
1045
|
+
return nil
|
|
1046
|
+
end
|
|
1047
|
+
|
|
1048
|
+
clp_data = class_permissions.as_json
|
|
1049
|
+
return nil if clp_data.empty?
|
|
1050
|
+
|
|
1051
|
+
schema_update = { "classLevelPermissions" => clp_data }
|
|
1052
|
+
client.update_schema(parse_class, schema_update)
|
|
1053
|
+
end
|
|
1054
|
+
|
|
1055
|
+
alias_method :update_class_permissions!, :update_clp!
|
|
1056
|
+
|
|
1057
|
+
# @!endgroup
|
|
1058
|
+
|
|
1059
|
+
end # << self
|
|
1060
|
+
|
|
1061
|
+
# @return [String] the Parse class for this object.
|
|
1062
|
+
# @see Parse::Object.parse_class
|
|
1063
|
+
def parse_class
|
|
1064
|
+
self.class.parse_class
|
|
1065
|
+
end
|
|
1066
|
+
|
|
1067
|
+
alias_method :className, :parse_class
|
|
1068
|
+
|
|
1069
|
+
# @return [Hash] the schema structure for this Parse collection from the server.
|
|
1070
|
+
# @see Parse::Core::Schema
|
|
1071
|
+
def schema
|
|
1072
|
+
self.class.schema
|
|
1073
|
+
end
|
|
1074
|
+
|
|
1075
|
+
# @!group Field Filtering (CLP)
|
|
1076
|
+
|
|
1077
|
+
# Filter this object's fields based on Class-Level Permissions for a user.
|
|
1078
|
+
# Uses the CLP configured on the model class to determine which fields
|
|
1079
|
+
# should be visible to the given user/roles context.
|
|
1080
|
+
#
|
|
1081
|
+
# This is useful for filtering webhook responses or API data before
|
|
1082
|
+
# sending to clients.
|
|
1083
|
+
#
|
|
1084
|
+
# @param user [Parse::User, String, nil] the user or user ID
|
|
1085
|
+
# @param roles [Array<String>] role names the user belongs to
|
|
1086
|
+
# @param authenticated [Boolean] whether the user is authenticated
|
|
1087
|
+
# @param clp [Parse::CLP, nil] optional CLP to use (defaults to class CLP)
|
|
1088
|
+
# @return [Hash] filtered data hash with protected fields removed
|
|
1089
|
+
#
|
|
1090
|
+
# @example Filter object for a specific user
|
|
1091
|
+
# song = Song.first
|
|
1092
|
+
# filtered = song.filter_for_user(current_user, roles: ["Member"])
|
|
1093
|
+
#
|
|
1094
|
+
# @example Filter for unauthenticated access
|
|
1095
|
+
# filtered = song.filter_for_user(nil)
|
|
1096
|
+
#
|
|
1097
|
+
# @see Parse::CLP#filter_fields
|
|
1098
|
+
def filter_for_user(user, roles: [], authenticated: nil, clp: nil)
|
|
1099
|
+
clp ||= self.class.class_permissions
|
|
1100
|
+
return as_json unless clp.present?
|
|
1101
|
+
|
|
1102
|
+
clp.filter_fields(as_json, user: user, roles: roles, authenticated: authenticated)
|
|
1103
|
+
end
|
|
1104
|
+
|
|
1105
|
+
# Filter an array of Parse objects or hashes for a user.
|
|
1106
|
+
# Class method that applies CLP filtering to multiple results.
|
|
1107
|
+
#
|
|
1108
|
+
# @param objects [Array<Parse::Object, Hash>] array of objects or hashes to filter
|
|
1109
|
+
# @param user [Parse::User, String, nil] the user or user ID
|
|
1110
|
+
# @param roles [Array<String>] role names the user belongs to
|
|
1111
|
+
# @param authenticated [Boolean] whether the user is authenticated
|
|
1112
|
+
# @param clp [Parse::CLP, nil] optional CLP to use (defaults to class CLP)
|
|
1113
|
+
# @return [Array<Hash>] filtered data hashes with protected fields removed
|
|
1114
|
+
#
|
|
1115
|
+
# @example Filter query results for a user
|
|
1116
|
+
# songs = Song.query(artist: "Beatles").results
|
|
1117
|
+
# filtered = Song.filter_results_for_user(songs, current_user, roles: user_roles)
|
|
1118
|
+
#
|
|
1119
|
+
# @see Parse::CLP#filter_fields
|
|
1120
|
+
def self.filter_results_for_user(objects, user, roles: [], authenticated: nil, clp: nil)
|
|
1121
|
+
clp ||= class_permissions
|
|
1122
|
+
return objects.map { |o| o.is_a?(Parse::Object) ? o.as_json : o } unless clp.present?
|
|
1123
|
+
|
|
1124
|
+
objects.map do |obj|
|
|
1125
|
+
data = obj.is_a?(Parse::Object) ? obj.as_json : obj
|
|
1126
|
+
clp.filter_fields(data, user: user, roles: roles, authenticated: authenticated)
|
|
1127
|
+
end
|
|
1128
|
+
end
|
|
1129
|
+
|
|
1130
|
+
# Fetch a user's roles for use with field filtering.
|
|
1131
|
+
# Convenience method to get role names that can be passed to filter methods.
|
|
1132
|
+
#
|
|
1133
|
+
# @param user [Parse::User] the user to get roles for
|
|
1134
|
+
# @return [Array<String>] role names (without "role:" prefix)
|
|
1135
|
+
#
|
|
1136
|
+
# @example Get roles and filter
|
|
1137
|
+
# roles = Song.roles_for_user(current_user)
|
|
1138
|
+
# filtered = song.filter_for_user(current_user, roles: roles)
|
|
1139
|
+
def self.roles_for_user(user)
|
|
1140
|
+
return [] unless user.is_a?(Parse::User) || user.is_a?(Parse::Pointer)
|
|
1141
|
+
return [] unless defined?(Parse::Role)
|
|
1142
|
+
|
|
1143
|
+
user_id = user.respond_to?(:id) ? user.id : user.to_s
|
|
1144
|
+
return [] if user_id.blank?
|
|
1145
|
+
|
|
1146
|
+
Parse::Role.all(users: user).map(&:name)
|
|
1147
|
+
rescue => e
|
|
1148
|
+
warn "[Parse] Error fetching roles for user: #{e.message}"
|
|
1149
|
+
[]
|
|
1150
|
+
end
|
|
1151
|
+
|
|
1152
|
+
# @!endgroup
|
|
1153
|
+
|
|
1154
|
+
# Core identification fields that are always included in serialization
|
|
1155
|
+
# unless strict: true is specified
|
|
1156
|
+
IDENTIFICATION_FIELDS = %w[id objectId __type className].freeze
|
|
1157
|
+
|
|
1158
|
+
# @return [Hash] a json-hash representing this object.
|
|
1159
|
+
# @param opts [Hash] options for serialization
|
|
1160
|
+
# @option opts [Boolean] :only_fetched when true (or when Parse.serialize_only_fetched_fields
|
|
1161
|
+
# is true and this option is not explicitly set to false), only serialize fields that
|
|
1162
|
+
# were fetched for partially fetched objects. This prevents autofetch during serialization.
|
|
1163
|
+
# @option opts [Array<Symbol,String>] :only limit serialization to these fields. By default,
|
|
1164
|
+
# identification fields (objectId, className, __type, id) are always included for proper
|
|
1165
|
+
# object identification. Use strict: true to disable this behavior.
|
|
1166
|
+
# @option opts [Array<Symbol,String>] :except exclude these fields from serialization
|
|
1167
|
+
# @option opts [Array<Symbol,String>] :exclude_keys alias for :except
|
|
1168
|
+
# @option opts [Array<Symbol,String>] :exclude alias for :except
|
|
1169
|
+
# @option opts [Boolean] :strict when true with :only, performs strict filtering without
|
|
1170
|
+
# automatically including identification fields. Default is false.
|
|
1171
|
+
def as_json(opts = nil)
|
|
1172
|
+
opts ||= {}
|
|
1173
|
+
|
|
1174
|
+
# Normalize :exclude_keys and :exclude to :except (alias support)
|
|
1175
|
+
if !opts[:except]
|
|
1176
|
+
if opts[:exclude_keys]
|
|
1177
|
+
opts = opts.merge(except: opts[:exclude_keys])
|
|
1178
|
+
elsif opts[:exclude]
|
|
1179
|
+
opts = opts.merge(except: opts[:exclude])
|
|
1180
|
+
end
|
|
1181
|
+
end
|
|
1182
|
+
|
|
1183
|
+
# When :only is specified without :strict, automatically include identification fields
|
|
1184
|
+
# so the serialized object can be properly identified
|
|
1185
|
+
if opts[:only] && !opts[:strict]
|
|
1186
|
+
only_keys = Array(opts[:only]).map(&:to_s)
|
|
1187
|
+
only_keys |= IDENTIFICATION_FIELDS
|
|
1188
|
+
opts = opts.merge(only: only_keys)
|
|
1189
|
+
end
|
|
1190
|
+
|
|
1191
|
+
# For selectively fetched objects (partial fetch), serialize only the fetched fields.
|
|
1192
|
+
# This takes priority over pointer detection because a partial fetch has actual data
|
|
1193
|
+
# even if it lacks timestamps (which would otherwise make it look like a pointer).
|
|
1194
|
+
# This behavior is controlled by:
|
|
1195
|
+
# 1. Per-call: opts[:only_fetched] (explicit true/false)
|
|
1196
|
+
# 2. Global: Parse.serialize_only_fetched_fields (default true)
|
|
1197
|
+
if has_selective_keys?
|
|
1198
|
+
# Determine if we should serialize only fetched fields
|
|
1199
|
+
only_fetched = opts.fetch(:only_fetched) { Parse.serialize_only_fetched_fields }
|
|
1200
|
+
|
|
1201
|
+
if only_fetched && !opts.key?(:only)
|
|
1202
|
+
# Build the :only list from fetched keys
|
|
1203
|
+
# Use the local field names which match the attribute methods
|
|
1204
|
+
only_keys = fetched_keys.map(&:to_s)
|
|
1205
|
+
# Always include Parse metadata fields for proper object identification
|
|
1206
|
+
only_keys |= IDENTIFICATION_FIELDS
|
|
1207
|
+
only_keys |= %w[created_at updated_at]
|
|
1208
|
+
opts = opts.merge(only: only_keys)
|
|
1209
|
+
end
|
|
1210
|
+
|
|
1211
|
+
changed_fields = changed_attributes
|
|
1212
|
+
return super(opts).delete_if { |k, v| v.nil? && !changed_fields.has_key?(k) }
|
|
1213
|
+
end
|
|
1214
|
+
|
|
1215
|
+
# When in pointer state (no data fetched, just an objectId), return the serialized
|
|
1216
|
+
# pointer hash (with __type, className, objectId) for proper JSON serialization
|
|
1217
|
+
return pointer.as_json(opts) if pointer?
|
|
1218
|
+
|
|
1219
|
+
changed_fields = changed_attributes
|
|
1220
|
+
super(opts).delete_if { |k, v| v.nil? && !changed_fields.has_key?(k) }
|
|
1221
|
+
end
|
|
1222
|
+
|
|
1223
|
+
private
|
|
1224
|
+
|
|
1225
|
+
# Override to return string keys for compatibility with ActiveModel's serialization.
|
|
1226
|
+
# ActiveModel::Serialization#serializable_hash uses string comparison for :only/:except
|
|
1227
|
+
# options, but our attributes method returns symbol keys.
|
|
1228
|
+
# @return [Array<String>] attribute names as strings
|
|
1229
|
+
# @!visibility private
|
|
1230
|
+
def attribute_names_for_serialization
|
|
1231
|
+
attributes.keys.map(&:to_s)
|
|
1232
|
+
end
|
|
1233
|
+
|
|
1234
|
+
public
|
|
1235
|
+
|
|
1236
|
+
# The main constructor for subclasses. It can take different parameter types
|
|
1237
|
+
# including a String and a JSON hash. Assume a `Post` class that inherits
|
|
1238
|
+
# from Parse::Object:
|
|
1239
|
+
# @note Should only be called with Parse::Object subclasses.
|
|
1240
|
+
# @overload new(id)
|
|
1241
|
+
# Create a new object with an objectId. This method is useful for creating
|
|
1242
|
+
# an unfetched object (pointer-state).
|
|
1243
|
+
# @example
|
|
1244
|
+
# Post.new "1234"
|
|
1245
|
+
# @param id [String] The object id.
|
|
1246
|
+
# @overload new(hash = {})
|
|
1247
|
+
# Create a new object with Parse JSON hash.
|
|
1248
|
+
# @example
|
|
1249
|
+
# # JSON hash from Parse
|
|
1250
|
+
# Post.new({"className" => "Post", "objectId" => "1234", "title" => "My Title"})
|
|
1251
|
+
#
|
|
1252
|
+
# post = Post.new title: "My Title"
|
|
1253
|
+
# post.title # => "My Title"
|
|
1254
|
+
#
|
|
1255
|
+
# @param hash [Hash] the hash representing the object.
|
|
1256
|
+
# Untrusted by default: keys in
|
|
1257
|
+
# {Parse::Properties::PROTECTED_INITIALIZE_KEYS} (+sessionToken+,
|
|
1258
|
+
# +_rperm+, +_wperm+, +_hashed_password+, +authData+, +roles+)
|
|
1259
|
+
# are filtered out even when an +objectId+ is present. This
|
|
1260
|
+
# closes the mass-assignment hole where +klass.new(attacker_params)+
|
|
1261
|
+
# on a hash that happens to include +objectId+ would overwrite
|
|
1262
|
+
# session tokens, ACLs, and auth data. Use {Parse::Object.build}
|
|
1263
|
+
# for trusted hydration from server JSON; it bypasses the filter.
|
|
1264
|
+
# @return [Parse::Object] a the corresponding Parse::Object or subclass.
|
|
1265
|
+
def initialize(opts = {})
|
|
1266
|
+
# Trusted hydration is signalled by the +@_trusted_init+ instance
|
|
1267
|
+
# variable rather than by a +trusted:+ keyword argument. Using a
|
|
1268
|
+
# keyword would break subclasses that override +initialize(*args)+
|
|
1269
|
+
# and call +super+ — Ruby 3 keyword-arg semantics would convert the
|
|
1270
|
+
# kwarg into a positional Hash through the variadic +*args+ splat
|
|
1271
|
+
# and the subsequent +super+ would arrive at this method with two
|
|
1272
|
+
# positional args. The internal hydration paths
|
|
1273
|
+
# ({Parse::Object.build}, {Parse::Pointer} autofetch,
|
|
1274
|
+
# {Parse::User#session}) +allocate+ the object, set the ivar, then
|
|
1275
|
+
# invoke +initialize+ so subclass overrides still fire and pick up
|
|
1276
|
+
# the trust signal here.
|
|
1277
|
+
trusted = @_trusted_init == true
|
|
1278
|
+
@_trusted_init = nil
|
|
1279
|
+
acl_owner_override = nil
|
|
1280
|
+
if opts.is_a?(String) #then it's the objectId
|
|
1281
|
+
@id = opts.to_s
|
|
1282
|
+
elsif opts.is_a?(Hash)
|
|
1283
|
+
# Pop the `:as` option (also accepts string key) before applying
|
|
1284
|
+
# attributes so it is not mistaken for a model property. This holds
|
|
1285
|
+
# the caller-supplied owner user for save-time ACL resolution.
|
|
1286
|
+
acl_owner_override = opts.delete(:as) || opts.delete("as")
|
|
1287
|
+
#if the objectId is provided we will consider the object pristine
|
|
1288
|
+
#and not track dirty items
|
|
1289
|
+
dirty_track = opts[Parse::Model::OBJECT_ID] || opts[:objectId] || opts[:id]
|
|
1290
|
+
# Always filter the narrow PROTECTED_INITIALIZE_KEYS set unless
|
|
1291
|
+
# the caller is a trusted hydration path. Decoupled from
|
|
1292
|
+
# dirty_track so an objectId-bearing hash from a controller,
|
|
1293
|
+
# JSON params, or cache rehydrator cannot mass-assign
|
|
1294
|
+
# sessionToken / _rperm / _wperm / _hashed_password / authData /
|
|
1295
|
+
# roles. The narrow list deliberately allows createdAt /
|
|
1296
|
+
# updatedAt / className / __type through so the legitimate
|
|
1297
|
+
# +Klass.new("objectId" => id, "createdAt" => ts, …)+
|
|
1298
|
+
# cache-rehydrate pattern keeps working.
|
|
1299
|
+
apply_attributes!(opts,
|
|
1300
|
+
dirty_track: !dirty_track,
|
|
1301
|
+
filter_protected: !trusted,
|
|
1302
|
+
protected_set: Parse::Properties::PROTECTED_INITIALIZE_KEYS)
|
|
1303
|
+
end
|
|
1304
|
+
|
|
1305
|
+
# If the caller did not set an ACL via opts, stamp the class default ACL
|
|
1306
|
+
# (the policy's fallback half) so `obj.acl` reads sensibly pre-save.
|
|
1307
|
+
# We mark the object as "ACL-pristine": the save-time resolver
|
|
1308
|
+
# (#_resolve_default_acl) may upgrade this to an owner-only ACL if an
|
|
1309
|
+
# `as:` user or owner field is resolvable. Any explicit caller change
|
|
1310
|
+
# via `acl=` flips pristine off via #acl_will_change!.
|
|
1311
|
+
#
|
|
1312
|
+
# Built-in Parse classes (User, Installation, Session, Role, …) are
|
|
1313
|
+
# exempt: the SDK leaves their `acl` untouched (nil) so the save body
|
|
1314
|
+
# omits the `ACL` field and Parse Server applies its own per-class
|
|
1315
|
+
# defaults. Most importantly this lets `_User` get the standard
|
|
1316
|
+
# self-write-plus-public-read ACL on signup; stamping any value from
|
|
1317
|
+
# the SDK side (even `{}`) overrides that and locks the new user out
|
|
1318
|
+
# of editing their own profile without the master key.
|
|
1319
|
+
acl_was_user_supplied = !self.acl.nil?
|
|
1320
|
+
unless self.class.builtin_acl_default_active?
|
|
1321
|
+
self.acl = self.class.default_acls.as_json if self.acl.nil?
|
|
1322
|
+
end
|
|
1323
|
+
@_acl_pristine = !acl_was_user_supplied
|
|
1324
|
+
@_acl_owner_override = acl_owner_override
|
|
1325
|
+
|
|
1326
|
+
# One-time per-class permissive-default warning. Fires only when the
|
|
1327
|
+
# effective policy is :public or :owner_else_public.
|
|
1328
|
+
self.class._warn_permissive_acl_default_once
|
|
1329
|
+
|
|
1330
|
+
# do not apply defaults on a pointer because it will stop it from being
|
|
1331
|
+
# a pointer and will cause its field to be autofetched (for sync).
|
|
1332
|
+
# Note: apply_defaults! already skips unfetched fields on selectively fetched objects.
|
|
1333
|
+
if !pointer?
|
|
1334
|
+
apply_defaults!
|
|
1335
|
+
end
|
|
1336
|
+
|
|
1337
|
+
# clear changes AFTER applying defaults, so fields set by defaults
|
|
1338
|
+
# are not marked dirty when fetching with specific keys
|
|
1339
|
+
clear_changes! if @id.present? #then it was an import
|
|
1340
|
+
# do not call super since it is Pointer subclass
|
|
1341
|
+
end
|
|
1342
|
+
|
|
1343
|
+
# force apply default values for any properties defined with default values.
|
|
1344
|
+
# @return [Array] list of default fields
|
|
1345
|
+
def apply_defaults!
|
|
1346
|
+
self.class.defaults_list.each do |key|
|
|
1347
|
+
# Skip applying defaults to unfetched fields on selectively fetched objects.
|
|
1348
|
+
# This preserves the ability to autofetch when the field is accessed.
|
|
1349
|
+
next if has_selective_keys? && !field_was_fetched?(key)
|
|
1350
|
+
|
|
1351
|
+
send(key) # should call set default proc/values if nil
|
|
1352
|
+
end
|
|
1353
|
+
end
|
|
1354
|
+
|
|
1355
|
+
# Helper method to create a Parse::Pointer object for a given id.
|
|
1356
|
+
# @param id [String] The objectId
|
|
1357
|
+
# @return [Parse::Pointer] a pointer object corresponding to this class and id.
|
|
1358
|
+
def self.pointer(id)
|
|
1359
|
+
return nil if id.nil?
|
|
1360
|
+
Parse::Pointer.new self.parse_class, id
|
|
1361
|
+
end
|
|
1362
|
+
|
|
1363
|
+
# Determines if this object has been saved to the Parse database. If an object has
|
|
1364
|
+
# pending changes, then it is considered to not yet be persisted.
|
|
1365
|
+
# @return [Boolean] true if this object has not been saved.
|
|
1366
|
+
def persisted?
|
|
1367
|
+
changed? == false && !(@id.nil? || @created_at.nil? || @updated_at.nil? || @acl.nil?)
|
|
1368
|
+
end
|
|
1369
|
+
|
|
1370
|
+
# Force reload from the database and replace any local fields with data from
|
|
1371
|
+
# the persistent store. By default, bypasses cache reads but updates the cache
|
|
1372
|
+
# with fresh data (write-only mode) so future cached reads get the latest data.
|
|
1373
|
+
# @param opts [Hash] a set of options to send to fetch!
|
|
1374
|
+
# @option opts [Boolean, Symbol] :cache (:write_only) caching mode:
|
|
1375
|
+
# - :write_only (default) - skip cache read, but update cache with fresh data
|
|
1376
|
+
# - true - read from and write to cache
|
|
1377
|
+
# - false - completely bypass cache (no read or write)
|
|
1378
|
+
# @see Fetching#fetch!
|
|
1379
|
+
# @example Reload with fresh data (default - updates cache)
|
|
1380
|
+
# song.reload!
|
|
1381
|
+
# @example Reload with full caching (may return cached data)
|
|
1382
|
+
# song.reload!(cache: true)
|
|
1383
|
+
# @example Reload completely bypassing cache
|
|
1384
|
+
# song.reload!(cache: false)
|
|
1385
|
+
def reload!(**opts)
|
|
1386
|
+
# Default to write-only cache mode - reload always gets fresh data
|
|
1387
|
+
# but updates cache for future cached reads. Controlled by feature flag.
|
|
1388
|
+
unless opts.key?(:cache)
|
|
1389
|
+
opts[:cache] = Parse.cache_write_on_fetch ? :write_only : false
|
|
1390
|
+
end
|
|
1391
|
+
# get the values from the persistence layer
|
|
1392
|
+
fetch!(**opts)
|
|
1393
|
+
clear_changes!
|
|
1394
|
+
end
|
|
1395
|
+
|
|
1396
|
+
# clears all dirty tracking information
|
|
1397
|
+
def clear_changes!
|
|
1398
|
+
clear_changes_information
|
|
1399
|
+
# Clear the ACL snapshot used for proper acl_was tracking
|
|
1400
|
+
@_acl_snapshot_before_change = nil
|
|
1401
|
+
end
|
|
1402
|
+
|
|
1403
|
+
# An object is considered new until it has been successfully persisted to
|
|
1404
|
+
# the server. "Persisted" means the server has returned a `createdAt`
|
|
1405
|
+
# timestamp, which only happens after a successful create. Checking
|
|
1406
|
+
# @id alone is not sufficient: the `parse_reference precompute: true`
|
|
1407
|
+
# path assigns @id client-side in a `before_create` callback, so an
|
|
1408
|
+
# @id-only check would flip mid-callback-chain and confuse user code
|
|
1409
|
+
# (validation `on: :create / :update`, beforeSave handlers, etc.).
|
|
1410
|
+
# Treating an object as "new" until createdAt arrives keeps semantics
|
|
1411
|
+
# stable from the first `before_save` through the end of `after_create`.
|
|
1412
|
+
# @return [Boolean] true if the object has not yet been persisted.
|
|
1413
|
+
def new?
|
|
1414
|
+
@id.blank? || @created_at.nil?
|
|
1415
|
+
end
|
|
1416
|
+
|
|
1417
|
+
# Override valid? to run validation callbacks.
|
|
1418
|
+
# This wraps the standard ActiveModel validation with our custom :validation callbacks.
|
|
1419
|
+
# @param context [Symbol, nil] validation context (same as ActiveModel)
|
|
1420
|
+
# @return [Boolean] true if the object passes all validations
|
|
1421
|
+
def valid?(context = nil)
|
|
1422
|
+
result = true
|
|
1423
|
+
run_callbacks :validation do
|
|
1424
|
+
result = super(context)
|
|
1425
|
+
end
|
|
1426
|
+
result
|
|
1427
|
+
end
|
|
1428
|
+
|
|
1429
|
+
# Existed returns true if the object had existed before *its last save
|
|
1430
|
+
# operation*. This method returns false if the {Parse::Object#created_at}
|
|
1431
|
+
# and {Parse::Object#updated_at} dates of an object are equal, implyiny this
|
|
1432
|
+
# object has been newly created and saved (especially in an afterSave hook).
|
|
1433
|
+
#
|
|
1434
|
+
# This is a helper method in a webhook afterSave to know
|
|
1435
|
+
# if this object was recently saved in the beforeSave webhook. Checking for
|
|
1436
|
+
# {Parse::Object#existed?} == false in an afterSave hook, is equivalent to using
|
|
1437
|
+
# {Parse::Object#new?} in a beforeSave hook.
|
|
1438
|
+
# @note You should not use this method inside a beforeSave webhook.
|
|
1439
|
+
# @return [Boolean] true iff the last beforeSave successfully saved this object for the first time.
|
|
1440
|
+
def existed?
|
|
1441
|
+
if @id.blank? || @created_at.blank? || @updated_at.blank?
|
|
1442
|
+
return false
|
|
1443
|
+
end
|
|
1444
|
+
created_at != updated_at
|
|
1445
|
+
end
|
|
1446
|
+
|
|
1447
|
+
# Returns whether this object was fetched with specific keys (selective fetch).
|
|
1448
|
+
# When selectively fetched, accessing unfetched fields will trigger an autofetch.
|
|
1449
|
+
# This is an internal method used for autofetch logic.
|
|
1450
|
+
# @return [Boolean] true if the object was fetched with specific keys.
|
|
1451
|
+
# @api private
|
|
1452
|
+
def has_selective_keys?
|
|
1453
|
+
@_fetched_keys&.any? || false
|
|
1454
|
+
end
|
|
1455
|
+
|
|
1456
|
+
# Returns whether this object was fetched with specific keys (partial/selective fetch).
|
|
1457
|
+
# When partially fetched, only the specified keys are available and accessing other
|
|
1458
|
+
# fields will trigger an autofetch. Returns false for pointers and fully fetched objects.
|
|
1459
|
+
# @return [Boolean] true if the object was fetched with specific keys.
|
|
1460
|
+
def partially_fetched?
|
|
1461
|
+
!pointer? && has_selective_keys?
|
|
1462
|
+
end
|
|
1463
|
+
|
|
1464
|
+
# Returns whether this object is fully fetched with all fields available.
|
|
1465
|
+
# Returns false if the object is a pointer or was fetched with specific keys.
|
|
1466
|
+
# @return [Boolean] true if the object is fully fetched.
|
|
1467
|
+
def fully_fetched?
|
|
1468
|
+
!pointer? && !has_selective_keys?
|
|
1469
|
+
end
|
|
1470
|
+
|
|
1471
|
+
# Returns whether this object has been fetched from the server (fully or partially).
|
|
1472
|
+
# Overrides Pointer#fetched? to return true for any object with data.
|
|
1473
|
+
# @return [Boolean] true if the object has data (not just a pointer).
|
|
1474
|
+
def fetched?
|
|
1475
|
+
!pointer?
|
|
1476
|
+
end
|
|
1477
|
+
|
|
1478
|
+
# Returns the array of keys that were fetched for this object.
|
|
1479
|
+
# Empty array means the object was fully fetched.
|
|
1480
|
+
# Returns a frozen duplicate to prevent external mutation.
|
|
1481
|
+
# @return [Array<Symbol>] the keys that were fetched.
|
|
1482
|
+
def fetched_keys
|
|
1483
|
+
(@_fetched_keys || []).dup.freeze
|
|
1484
|
+
end
|
|
1485
|
+
|
|
1486
|
+
# Disables autofetch for this object instance.
|
|
1487
|
+
# Useful for preventing automatic network requests.
|
|
1488
|
+
# @return [void]
|
|
1489
|
+
def disable_autofetch!
|
|
1490
|
+
@_autofetch_disabled = true
|
|
1491
|
+
end
|
|
1492
|
+
|
|
1493
|
+
# Enables autofetch for this object instance (default behavior).
|
|
1494
|
+
# @return [void]
|
|
1495
|
+
def enable_autofetch!
|
|
1496
|
+
@_autofetch_disabled = false
|
|
1497
|
+
end
|
|
1498
|
+
|
|
1499
|
+
# Returns whether autofetch is disabled for this instance.
|
|
1500
|
+
# @return [Boolean] true if autofetch is disabled
|
|
1501
|
+
def autofetch_disabled?
|
|
1502
|
+
@_autofetch_disabled == true
|
|
1503
|
+
end
|
|
1504
|
+
|
|
1505
|
+
# Sets the fetched keys for this object. Used internally when building
|
|
1506
|
+
# objects from partial fetch queries.
|
|
1507
|
+
# @param keys [Array] the keys that were fetched
|
|
1508
|
+
# @return [Array] the stored keys
|
|
1509
|
+
def fetched_keys=(keys)
|
|
1510
|
+
if keys.nil? || keys.empty?
|
|
1511
|
+
@_fetched_keys = nil
|
|
1512
|
+
else
|
|
1513
|
+
# Always include :id and convert to symbols
|
|
1514
|
+
@_fetched_keys = keys.map { |k| Parse::Query.format_field(k).to_sym }
|
|
1515
|
+
@_fetched_keys << :id unless @_fetched_keys.include?(:id)
|
|
1516
|
+
@_fetched_keys << :objectId unless @_fetched_keys.include?(:objectId)
|
|
1517
|
+
@_fetched_keys.uniq!
|
|
1518
|
+
end
|
|
1519
|
+
@_fetched_keys
|
|
1520
|
+
end
|
|
1521
|
+
|
|
1522
|
+
# Returns whether a specific field was fetched for this object.
|
|
1523
|
+
# Base keys (id, created_at, updated_at) are always considered fetched.
|
|
1524
|
+
# @param key [Symbol, String] the field name to check
|
|
1525
|
+
# @return [Boolean] true if the field was fetched or if object is fully fetched.
|
|
1526
|
+
def field_was_fetched?(key)
|
|
1527
|
+
# If not partially fetched (i.e., still a pointer), all fields are NOT fetched
|
|
1528
|
+
return false if pointer?
|
|
1529
|
+
|
|
1530
|
+
# If no selective keys were specified, this is a fully fetched object
|
|
1531
|
+
# All fields are considered fetched
|
|
1532
|
+
return true unless has_selective_keys?
|
|
1533
|
+
|
|
1534
|
+
key = key.to_sym
|
|
1535
|
+
# Base keys are always considered fetched
|
|
1536
|
+
return true if Parse::Properties::BASE_KEYS.include?(key)
|
|
1537
|
+
return true if key == :acl || key == :ACL
|
|
1538
|
+
|
|
1539
|
+
# Check both local key and remote field name
|
|
1540
|
+
# Convert remote_key to symbol for consistent comparison
|
|
1541
|
+
remote_key = self.field_map[key]&.to_sym
|
|
1542
|
+
@_fetched_keys.include?(key) || (remote_key && @_fetched_keys.include?(remote_key))
|
|
1543
|
+
end
|
|
1544
|
+
|
|
1545
|
+
# Returns the nested fetched keys map for building nested objects.
|
|
1546
|
+
# @return [Hash] map of field names to their fetched keys
|
|
1547
|
+
def nested_fetched_keys
|
|
1548
|
+
@_nested_fetched_keys || {}
|
|
1549
|
+
end
|
|
1550
|
+
|
|
1551
|
+
# Sets the nested fetched keys map for building nested objects.
|
|
1552
|
+
# @param keys_map [Hash] map of field names to their fetched keys
|
|
1553
|
+
# @return [Hash] the stored map
|
|
1554
|
+
def nested_fetched_keys=(keys_map)
|
|
1555
|
+
@_nested_fetched_keys = keys_map.is_a?(Hash) ? keys_map : nil
|
|
1556
|
+
end
|
|
1557
|
+
|
|
1558
|
+
# Gets the fetched keys for a specific nested field.
|
|
1559
|
+
# @param field_name [Symbol, String] the field name
|
|
1560
|
+
# @return [Array, nil] the fetched keys for the nested object, or nil if not specified
|
|
1561
|
+
def nested_keys_for(field_name)
|
|
1562
|
+
return nil unless @_nested_fetched_keys.present?
|
|
1563
|
+
field_name = field_name.to_sym
|
|
1564
|
+
@_nested_fetched_keys[field_name]
|
|
1565
|
+
end
|
|
1566
|
+
|
|
1567
|
+
# Clears all partial fetch tracking state.
|
|
1568
|
+
# Called after successful save since server returns updated object.
|
|
1569
|
+
# @return [void]
|
|
1570
|
+
def clear_partial_fetch_state!
|
|
1571
|
+
@_fetched_keys = nil
|
|
1572
|
+
@_nested_fetched_keys = nil
|
|
1573
|
+
end
|
|
1574
|
+
|
|
1575
|
+
# Run after_create callbacks for this object.
|
|
1576
|
+
# This method is called by webhook handlers when an object is created.
|
|
1577
|
+
# @return [Boolean] true if callbacks executed successfully
|
|
1578
|
+
def run_after_create_callbacks
|
|
1579
|
+
run_callbacks_from_list(self.class._create_callbacks, :after)
|
|
1580
|
+
end
|
|
1581
|
+
|
|
1582
|
+
# Run after_save callbacks for this object.
|
|
1583
|
+
# This method is called by webhook handlers when an object is saved.
|
|
1584
|
+
# @return [Boolean] true if callbacks executed successfully
|
|
1585
|
+
def run_after_save_callbacks
|
|
1586
|
+
run_callbacks_from_list(self.class._save_callbacks, :after)
|
|
1587
|
+
end
|
|
1588
|
+
|
|
1589
|
+
# Run after_destroy callbacks for this object.
|
|
1590
|
+
# This method is called by webhook handlers when an object is deleted.
|
|
1591
|
+
# @return [Boolean] true if callbacks executed successfully
|
|
1592
|
+
def run_after_delete_callbacks
|
|
1593
|
+
run_callbacks_from_list(self.class._destroy_callbacks, :after)
|
|
1594
|
+
end
|
|
1595
|
+
|
|
1596
|
+
# Returns a hash of all the changes that have been made to the object. By default
|
|
1597
|
+
# changes to the Parse::Properties::BASE_KEYS are ignored unless you pass true as
|
|
1598
|
+
# an argument.
|
|
1599
|
+
# @param include_all [Boolean] whether to include all keys in result.
|
|
1600
|
+
# @return [Hash] a hash containing only the change information.
|
|
1601
|
+
# @see Properties::BASE_KEYS
|
|
1602
|
+
def updates(include_all = false)
|
|
1603
|
+
h = {}
|
|
1604
|
+
changed.each do |key|
|
|
1605
|
+
next if include_all == false && Parse::Properties::BASE_KEYS.include?(key.to_sym)
|
|
1606
|
+
# lookup the remote Parse field name incase it is different from the local attribute name
|
|
1607
|
+
remote_field = self.field_map[key.to_sym] || key
|
|
1608
|
+
h[remote_field] = send key
|
|
1609
|
+
# make an exception to Parse::Objects, we should return a pointer to them instead
|
|
1610
|
+
h[remote_field] = h[remote_field].parse_pointers if h[remote_field].is_a?(Parse::PointerCollectionProxy)
|
|
1611
|
+
h[remote_field] = h[remote_field].pointer if h[remote_field].respond_to?(:pointer)
|
|
1612
|
+
end
|
|
1613
|
+
h
|
|
1614
|
+
end
|
|
1615
|
+
|
|
1616
|
+
# Locally restores the previous state of the object and clears all dirty
|
|
1617
|
+
# tracking information.
|
|
1618
|
+
# @note This does not reload the object from the persistent store, for this use "reload!" instead.
|
|
1619
|
+
# @see #reload!
|
|
1620
|
+
def rollback!
|
|
1621
|
+
restore_attributes
|
|
1622
|
+
end
|
|
1623
|
+
|
|
1624
|
+
# Overrides ActiveModel::Validations#validate! instance method.
|
|
1625
|
+
# It runs all validations for this object. If validation fails,
|
|
1626
|
+
# it raises ActiveModel::ValidationError otherwise it returns the object.
|
|
1627
|
+
# @raise ActiveModel::ValidationError
|
|
1628
|
+
# @see ActiveModel::Validations#validate!
|
|
1629
|
+
# @return [self] self the object if validation passes.
|
|
1630
|
+
def validate!
|
|
1631
|
+
super
|
|
1632
|
+
self
|
|
1633
|
+
end
|
|
1634
|
+
|
|
1635
|
+
# This method creates a new object of the same instance type with a copy of
|
|
1636
|
+
# all the properties of the current instance. This is useful when you want
|
|
1637
|
+
# to create a duplicate record.
|
|
1638
|
+
# @return [Parse::Object] a twin copy of the object without the objectId
|
|
1639
|
+
def twin
|
|
1640
|
+
h = self.as_json
|
|
1641
|
+
h.delete(Parse::Model::OBJECT_ID)
|
|
1642
|
+
h.delete(:objectId)
|
|
1643
|
+
h.delete(:id)
|
|
1644
|
+
self.class.new h
|
|
1645
|
+
end
|
|
1646
|
+
|
|
1647
|
+
# @return [String] a pretty-formatted JSON string
|
|
1648
|
+
# @see JSON.pretty_generate
|
|
1649
|
+
def pretty
|
|
1650
|
+
JSON.pretty_generate(as_json)
|
|
1651
|
+
end
|
|
1652
|
+
|
|
1653
|
+
# clear all change and dirty tracking information.
|
|
1654
|
+
def clear_attribute_change!(atts)
|
|
1655
|
+
clear_attribute_changes(atts)
|
|
1656
|
+
end
|
|
1657
|
+
|
|
1658
|
+
# Method used for decoding JSON objects into their corresponding Object subclasses.
|
|
1659
|
+
# The first parameter is a hash containing the object data and the second parameter is the
|
|
1660
|
+
# name of the table / class if it is known. If it is not known, we we try and determine it
|
|
1661
|
+
# by checking the "className" or :className entries in the hash.
|
|
1662
|
+
# @note If a Parse class object hash is encoutered for which we don't have a
|
|
1663
|
+
# corresponding Parse::Object subclass for, a Parse::Pointer will be returned instead.
|
|
1664
|
+
#
|
|
1665
|
+
# @example
|
|
1666
|
+
# # assume you have defined Post subclass
|
|
1667
|
+
# post = Parse::Object.build({"className" => "Post", "objectId" => '1234'})
|
|
1668
|
+
# post # => #<Post:....>
|
|
1669
|
+
#
|
|
1670
|
+
# # if you know the table name
|
|
1671
|
+
# post = Parse::Object.build({"title" => "My Title"}, "Post")
|
|
1672
|
+
# # or
|
|
1673
|
+
# post = Post.build({"title" => "My Title"})
|
|
1674
|
+
# @param json [Hash] a JSON hash that contains a Parse object.
|
|
1675
|
+
# @param table [String] the Parse class for this hash. If not passed it will be detected.
|
|
1676
|
+
# @param fetched_keys [Array] optional array of keys that were fetched (for partial fetch tracking).
|
|
1677
|
+
# @param nested_fetched_keys [Hash] optional map of field names to their fetched keys for nested objects.
|
|
1678
|
+
# @return [Parse::Object] an instance of the Parse subclass
|
|
1679
|
+
def self.build(json, table = nil, fetched_keys: nil, nested_fetched_keys: nil)
|
|
1680
|
+
# Precedence (most → least authoritative):
|
|
1681
|
+
# 1. Caller-supplied +table+ — caller knows the expected class
|
|
1682
|
+
# (e.g. webhook payload routed to a typed handler, has_many that
|
|
1683
|
+
# knows its declared target class).
|
|
1684
|
+
# 2. The subclass +parse_class+ when invoked on a Parse::Object
|
|
1685
|
+
# subclass directly (Song.build(json)).
|
|
1686
|
+
# 3. The className inside the JSON — only trusted when neither of
|
|
1687
|
+
# the above is available (e.g. base-class +Parse::Object.build+
|
|
1688
|
+
# on untyped JSON).
|
|
1689
|
+
# Warn on mismatch between an explicit caller class and the
|
|
1690
|
+
# payload-supplied className so type-confusion attacks surface in
|
|
1691
|
+
# logs.
|
|
1692
|
+
incoming_class = nil
|
|
1693
|
+
if json.is_a?(Hash)
|
|
1694
|
+
incoming_class = json[Parse::Model::KEY_CLASS_NAME] || json[:className]
|
|
1695
|
+
end
|
|
1696
|
+
className = table
|
|
1697
|
+
if className.nil? && parse_class != BASE_OBJECT_CLASS
|
|
1698
|
+
className = parse_class
|
|
1699
|
+
end
|
|
1700
|
+
className ||= incoming_class
|
|
1701
|
+
if className && incoming_class && incoming_class != className
|
|
1702
|
+
warn "[Parse::Object.build] expected className=#{className.inspect}, ignoring incoming className=#{incoming_class.inspect}"
|
|
1703
|
+
end
|
|
1704
|
+
if json.is_a?(Hash) && json["error"].present? && json["code"].present?
|
|
1705
|
+
warn "[Parse::Object] Detected object hash with 'error' and 'code' set. : #{json}"
|
|
1706
|
+
end
|
|
1707
|
+
return if className.nil?
|
|
1708
|
+
# we should do a reverse lookup on who is registered for a different class type
|
|
1709
|
+
# than their name with parse_class
|
|
1710
|
+
klass = Parse::Model.find_class className
|
|
1711
|
+
o = nil
|
|
1712
|
+
if klass.present?
|
|
1713
|
+
# when creating objects from Parse JSON data, don't use dirty tracking since
|
|
1714
|
+
# we are considering these objects as "pristine"
|
|
1715
|
+
o = klass.allocate
|
|
1716
|
+
|
|
1717
|
+
# Set BOTH nested_fetched_keys AND fetched_keys BEFORE initialize
|
|
1718
|
+
# to ensure partially_fetched? returns correct value during attribute application
|
|
1719
|
+
o.instance_variable_set(:@_nested_fetched_keys, nested_fetched_keys) if nested_fetched_keys.present?
|
|
1720
|
+
if fetched_keys.present?
|
|
1721
|
+
# Process fetched_keys like the setter does - convert to symbols and include :id
|
|
1722
|
+
processed_keys = fetched_keys.map { |k| Parse::Query.format_field(k).to_sym }
|
|
1723
|
+
processed_keys << :id unless processed_keys.include?(:id)
|
|
1724
|
+
processed_keys << :objectId unless processed_keys.include?(:objectId)
|
|
1725
|
+
processed_keys.uniq!
|
|
1726
|
+
o.instance_variable_set(:@_fetched_keys, processed_keys)
|
|
1727
|
+
end
|
|
1728
|
+
|
|
1729
|
+
# Trusted hydration: this path runs on server-side JSON (response
|
|
1730
|
+
# bodies, webhook payloads that have already been scrubbed,
|
|
1731
|
+
# autofetch results). Server responses legitimately include
|
|
1732
|
+
# protected keys like +sessionToken+, +_rperm+ that must populate
|
|
1733
|
+
# the in-memory object. Untrusted +klass.new(hash)+ callers
|
|
1734
|
+
# default to filter those keys. The +@_trusted_init+ ivar is the
|
|
1735
|
+
# signal — see {#initialize} for why we don't use a kwarg.
|
|
1736
|
+
o.instance_variable_set(:@_trusted_init, true)
|
|
1737
|
+
o.send(:initialize, json)
|
|
1738
|
+
else
|
|
1739
|
+
o = Parse::Pointer.new className, (json[Parse::Model::OBJECT_ID] || json[:objectId])
|
|
1740
|
+
end
|
|
1741
|
+
return o
|
|
1742
|
+
# rescue NameError => e
|
|
1743
|
+
# puts "Parse::Object.build constant class error: #{e}"
|
|
1744
|
+
# rescue Exception => e
|
|
1745
|
+
# puts "Parse::Object.build error: #{e}"
|
|
1746
|
+
end
|
|
1747
|
+
|
|
1748
|
+
# @!attribute id
|
|
1749
|
+
# @return [String] the value of Parse "objectId" field.
|
|
1750
|
+
property :id, field: :objectId
|
|
1751
|
+
|
|
1752
|
+
# @!attribute [r] created_at
|
|
1753
|
+
# @return [Date] the created_at date of the record in UTC Zulu iso 8601 with 3 millisecond format.
|
|
1754
|
+
property :created_at, :date
|
|
1755
|
+
|
|
1756
|
+
# @!attribute [r] updated_at
|
|
1757
|
+
# @return [Date] the updated_at date of the record in UTC Zulu iso 8601 with 3 millisecond format.
|
|
1758
|
+
property :updated_at, :date
|
|
1759
|
+
|
|
1760
|
+
# @!attribute acl
|
|
1761
|
+
# @return [ACL] the access control list (permissions) object for this record.
|
|
1762
|
+
property :acl, :acl, field: :ACL
|
|
1763
|
+
|
|
1764
|
+
# Save-time resolver for the declarative {acl_policy} default ACL.
|
|
1765
|
+
# Runs as a `before_save` callback. If the caller has not overridden the
|
|
1766
|
+
# ACL (no `acl=` since the init-time default stamp), resolves an owner
|
|
1767
|
+
# from `@_acl_owner_override` (the `as:` kwarg) or from the class's
|
|
1768
|
+
# declared owner field, and applies an owner-only ACL. Falls back to
|
|
1769
|
+
# the policy's else-half (`:public` or `:private`) when no owner is
|
|
1770
|
+
# resolvable.
|
|
1771
|
+
# @api private
|
|
1772
|
+
def _resolve_default_acl
|
|
1773
|
+
return true unless defined?(@_acl_pristine) && @_acl_pristine
|
|
1774
|
+
# Legacy classes that customize defaults via set_default_acl opt out
|
|
1775
|
+
# of the policy resolver: the init-time stamp already reflects the
|
|
1776
|
+
# caller's intent and we must not overwrite it.
|
|
1777
|
+
return true if self.class.acl_default_customized_by_set_default_acl?
|
|
1778
|
+
# Built-in Parse classes (User, Installation, Session, …) are exempt
|
|
1779
|
+
# by default; see the matching guard in #initialize. Parse Server
|
|
1780
|
+
# applies its own ACL defaults when the save body omits the `ACL`
|
|
1781
|
+
# field, and those defaults (e.g. `_User` → self-write + public read)
|
|
1782
|
+
# are the right answer in nearly every case. Applications that need
|
|
1783
|
+
# to customize a built-in's ACL policy do so by calling `acl_policy`
|
|
1784
|
+
# or `set_default_acl` on the class — that flips
|
|
1785
|
+
# `builtin_acl_default_active?` to false and re-enables both the
|
|
1786
|
+
# init-time stamp and this resolver for that class.
|
|
1787
|
+
return true if self.class.builtin_acl_default_active?
|
|
1788
|
+
policy = self.class.acl_policy_setting
|
|
1789
|
+
|
|
1790
|
+
owner = @_acl_owner_override if defined?(@_acl_owner_override)
|
|
1791
|
+
if owner.nil? && (field = self.class.acl_owner_field)
|
|
1792
|
+
owner = if field == :self
|
|
1793
|
+
# Self-referential ownership (Parse::User only — enforced at
|
|
1794
|
+
# declaration time). Pre-generate a Parse-compatible objectId
|
|
1795
|
+
# client-side so the ACL grant can reference the record's own
|
|
1796
|
+
# id in the same POST body that creates it. Skipped when the id
|
|
1797
|
+
# is already set (e.g. when re-saving an existing user, or when
|
|
1798
|
+
# parse_reference precompute already ran).
|
|
1799
|
+
@id = Parse::Core::ParseReference.generate_object_id if @id.blank?
|
|
1800
|
+
@id
|
|
1801
|
+
elsif respond_to?(field)
|
|
1802
|
+
send(field)
|
|
1803
|
+
end
|
|
1804
|
+
end
|
|
1805
|
+
owner_id = _resolve_acl_owner_id(owner)
|
|
1806
|
+
|
|
1807
|
+
target_acl = case policy
|
|
1808
|
+
when :public
|
|
1809
|
+
Parse::ACL.everyone(true, true)
|
|
1810
|
+
when :private
|
|
1811
|
+
Parse::ACL.private
|
|
1812
|
+
when :owner_else_public
|
|
1813
|
+
if owner_id
|
|
1814
|
+
acl = Parse::ACL.new
|
|
1815
|
+
acl.apply(owner_id, true, true)
|
|
1816
|
+
acl
|
|
1817
|
+
else
|
|
1818
|
+
Parse::ACL.everyone(true, true)
|
|
1819
|
+
end
|
|
1820
|
+
when :owner_else_private
|
|
1821
|
+
if owner_id
|
|
1822
|
+
acl = Parse::ACL.new
|
|
1823
|
+
acl.apply(owner_id, true, true)
|
|
1824
|
+
acl
|
|
1825
|
+
else
|
|
1826
|
+
Parse::ACL.private
|
|
1827
|
+
end
|
|
1828
|
+
end
|
|
1829
|
+
|
|
1830
|
+
# Only re-stamp if the resolved ACL differs from the init-time stamp;
|
|
1831
|
+
# this avoids an unnecessary dirty mark on the acl field for `:public`
|
|
1832
|
+
# / `:private` policies where the init stamp already matches.
|
|
1833
|
+
if @acl.nil? || @acl.as_json != target_acl.as_json
|
|
1834
|
+
self.acl = target_acl.as_json
|
|
1835
|
+
end
|
|
1836
|
+
# @_acl_pristine is now false via #acl_will_change! (when re-stamped)
|
|
1837
|
+
# or it remains true (when nothing needed to change); either way the
|
|
1838
|
+
# resolver has done its job and need not run again. Return a non-false
|
|
1839
|
+
# value so the save callback chain is not halted by the model's
|
|
1840
|
+
# terminator (`result_lambda.call == false`).
|
|
1841
|
+
@_acl_pristine = false
|
|
1842
|
+
true
|
|
1843
|
+
end
|
|
1844
|
+
|
|
1845
|
+
# @api private
|
|
1846
|
+
# Resolves an `as:` value or owner-field pointer to an objectId string.
|
|
1847
|
+
# Strictly type-gated to Parse::User-shaped inputs to prevent accidental
|
|
1848
|
+
# ACL grants to non-user records (Roles use `role:` ACL keys, not raw
|
|
1849
|
+
# objectIds; pointers to non-User classes would silently grant access to
|
|
1850
|
+
# whatever record happens to share that objectId in the User collection).
|
|
1851
|
+
# Accepted forms:
|
|
1852
|
+
# - Parse::User instance
|
|
1853
|
+
# - Parse::Pointer with parse_class == "_User"
|
|
1854
|
+
# - Raw objectId String (caller's responsibility to ensure it is a user id)
|
|
1855
|
+
# Anything else returns nil and the policy falls through to its else-half.
|
|
1856
|
+
def _resolve_acl_owner_id(owner)
|
|
1857
|
+
return nil if owner.nil?
|
|
1858
|
+
return nil if owner.respond_to?(:empty?) && owner.empty?
|
|
1859
|
+
if owner.is_a?(Parse::Pointer)
|
|
1860
|
+
return nil unless owner.parse_class == Parse::Model::CLASS_USER
|
|
1861
|
+
return owner.id if owner.id.present?
|
|
1862
|
+
return nil
|
|
1863
|
+
end
|
|
1864
|
+
return owner if owner.is_a?(String) && owner.present?
|
|
1865
|
+
nil
|
|
1866
|
+
end
|
|
1867
|
+
|
|
1868
|
+
set_callback :save, :before, :_resolve_default_acl
|
|
1869
|
+
|
|
1870
|
+
# Override acl_will_change! to capture a snapshot of the ACL before modification.
|
|
1871
|
+
# This is necessary because ACL is a mutable object that can be modified in place
|
|
1872
|
+
# (via apply, apply_role, etc.). Without this, acl_was would return a reference
|
|
1873
|
+
# to the same object as acl, making them appear identical after in-place changes.
|
|
1874
|
+
#
|
|
1875
|
+
# Also clears the ACL-pristine flag so the save-time default-ACL resolver
|
|
1876
|
+
# leaves caller-set ACLs alone. The initial default stamp performed in
|
|
1877
|
+
# {#initialize} is excluded by re-asserting `@_acl_pristine = true` after
|
|
1878
|
+
# the stamp, so this hook can safely treat any subsequent change as a
|
|
1879
|
+
# caller intent to override.
|
|
1880
|
+
# @api private
|
|
1881
|
+
def acl_will_change!
|
|
1882
|
+
# Only capture snapshot on the first change (before any modifications)
|
|
1883
|
+
unless defined?(@_acl_snapshot_before_change) && @_acl_snapshot_before_change
|
|
1884
|
+
# Deep copy the ACL by creating a new one from its JSON representation
|
|
1885
|
+
@_acl_snapshot_before_change = @acl ? Parse::ACL.new(@acl.as_json) : Parse::ACL.new
|
|
1886
|
+
end
|
|
1887
|
+
@_acl_pristine = false if defined?(@_acl_pristine)
|
|
1888
|
+
super
|
|
1889
|
+
end
|
|
1890
|
+
|
|
1891
|
+
# EnhancedChangeTracking defines acl_was via define_method when
|
|
1892
|
+
# `property :acl` is processed above. Remove that definition so the
|
|
1893
|
+
# explicit override below does not emit "method redefined" under ruby -W.
|
|
1894
|
+
# The override is intentional - ACL needs snapshot-based dirty tracking
|
|
1895
|
+
# because it is a mutable object.
|
|
1896
|
+
remove_method(:acl_was) if method_defined?(:acl_was, false)
|
|
1897
|
+
|
|
1898
|
+
# Override acl_was to return the captured snapshot instead of the reference
|
|
1899
|
+
# stored by ActiveModel's dirty tracking.
|
|
1900
|
+
# @return [Parse::ACL] the ACL value before any changes were made.
|
|
1901
|
+
def acl_was
|
|
1902
|
+
# If we have a snapshot, return it; otherwise fall back to ActiveModel's behavior
|
|
1903
|
+
if defined?(@_acl_snapshot_before_change) && @_acl_snapshot_before_change
|
|
1904
|
+
@_acl_snapshot_before_change
|
|
1905
|
+
else
|
|
1906
|
+
super
|
|
1907
|
+
end
|
|
1908
|
+
end
|
|
1909
|
+
|
|
1910
|
+
# Override acl_changed? to compare actual ACL content, not just object references.
|
|
1911
|
+
# This ensures that setting an ACL to identical values doesn't mark it as changed.
|
|
1912
|
+
# @return [Boolean] true only if the ACL content has actually changed.
|
|
1913
|
+
def acl_changed?
|
|
1914
|
+
# First check if ActiveModel thinks it changed
|
|
1915
|
+
return false unless super
|
|
1916
|
+
# Then verify the content actually changed by comparing JSON representations
|
|
1917
|
+
acl_was_json = acl_was.respond_to?(:as_json) ? acl_was.as_json : acl_was
|
|
1918
|
+
acl_current_json = @acl&.respond_to?(:as_json) ? @acl.as_json : @acl
|
|
1919
|
+
acl_was_json != acl_current_json
|
|
1920
|
+
end
|
|
1921
|
+
|
|
1922
|
+
# Override changed to filter out ACL when its content hasn't actually changed.
|
|
1923
|
+
# This ensures dirty? returns false when ACL is rebuilt to identical values.
|
|
1924
|
+
# For new objects, ACL is always included since it needs to be sent to the server.
|
|
1925
|
+
# @return [Array<String>] list of changed attribute names.
|
|
1926
|
+
def changed
|
|
1927
|
+
result = super.dup
|
|
1928
|
+
# If ACL is in the changed list but content is identical, remove it
|
|
1929
|
+
# BUT keep it if the object is new (needs to be sent to server)
|
|
1930
|
+
if result.include?("acl") && !new? && !acl_changed?
|
|
1931
|
+
result.delete("acl")
|
|
1932
|
+
end
|
|
1933
|
+
result
|
|
1934
|
+
end
|
|
1935
|
+
|
|
1936
|
+
# Override changed? to use our filtered changed list.
|
|
1937
|
+
# ActiveModel's changed? uses internal tracking that doesn't account for
|
|
1938
|
+
# ACL content comparison.
|
|
1939
|
+
# @return [Boolean] true if any attributes have changed.
|
|
1940
|
+
def changed?
|
|
1941
|
+
changed.any?
|
|
1942
|
+
end
|
|
1943
|
+
|
|
1944
|
+
# Access the value for a defined property through hash accessor. This method
|
|
1945
|
+
# returns nil if the key is not one of the defined properties for this Parse::Object
|
|
1946
|
+
# subclass.
|
|
1947
|
+
# @param key [String] the name of the property. This key must be in the {fields} hash.
|
|
1948
|
+
# @return [Object] the value for this key.
|
|
1949
|
+
def [](key)
|
|
1950
|
+
return nil unless self.class.fields[key.to_sym].present?
|
|
1951
|
+
send(key)
|
|
1952
|
+
end
|
|
1953
|
+
|
|
1954
|
+
# Set the value for a specific property through a hash accessor. This method
|
|
1955
|
+
# does nothing if key is not one of the defined properties for this Parse::Object
|
|
1956
|
+
# subclass.
|
|
1957
|
+
# @param key (see Parse::Object#[])
|
|
1958
|
+
# @param value [Object] the value to set this property.
|
|
1959
|
+
# @return [Object] the value passed in.
|
|
1960
|
+
def []=(key, value)
|
|
1961
|
+
return unless self.class.fields[key.to_sym].present?
|
|
1962
|
+
send("#{key}=", value)
|
|
1963
|
+
end
|
|
1964
|
+
|
|
1965
|
+
# Returns an array of property names (keys) for this Parse::Object.
|
|
1966
|
+
# Similar to Hash#keys, this method returns all the defined field names
|
|
1967
|
+
# for this object's class.
|
|
1968
|
+
# @return [Array<String>] an array of property names as strings.
|
|
1969
|
+
def keys
|
|
1970
|
+
self.class.fields.keys.map(&:to_s)
|
|
1971
|
+
end
|
|
1972
|
+
|
|
1973
|
+
# Check if a field has a value (is present and not nil).
|
|
1974
|
+
# @param key [String, Symbol] the name of the field to check.
|
|
1975
|
+
# @return [Boolean] true if the field has a non-nil value, false otherwise.
|
|
1976
|
+
def has?(key)
|
|
1977
|
+
return false unless self.class.fields[key.to_sym].present?
|
|
1978
|
+
value = send(key)
|
|
1979
|
+
!value.nil?
|
|
1980
|
+
end
|
|
1981
|
+
|
|
1982
|
+
private
|
|
1983
|
+
|
|
1984
|
+
# Helper to run a set of callbacks of a certain kind (e.g., :after)
|
|
1985
|
+
def run_callbacks_from_list(callbacks, kind)
|
|
1986
|
+
callbacks.select { |cb| cb.kind == kind }.each do |callback|
|
|
1987
|
+
# 'filter' can be a Symbol (method name), String (code), or Proc.
|
|
1988
|
+
case callback.filter
|
|
1989
|
+
when Symbol
|
|
1990
|
+
send(callback.filter)
|
|
1991
|
+
when Proc
|
|
1992
|
+
instance_exec(&callback.filter)
|
|
1993
|
+
when String
|
|
1994
|
+
instance_eval(callback.filter)
|
|
1995
|
+
end
|
|
1996
|
+
end
|
|
1997
|
+
true
|
|
1998
|
+
end
|
|
1999
|
+
end
|
|
2000
|
+
end
|
|
2001
|
+
|
|
2002
|
+
class Hash
|
|
2003
|
+
|
|
2004
|
+
# Turns a Parse formatted JSON hash into a Parse-Stack class object, if one is found.
|
|
2005
|
+
# This is equivalent to calling `Parse::Object.build` on the hash object itself, but allows
|
|
2006
|
+
# for doing this in loops, such as `map` when using symbol to proc. However, you can also use
|
|
2007
|
+
# the Array extension `Array#parse_objects` for doing that more safely.
|
|
2008
|
+
# @return [Parse::Object] A Parse::Object subclass represented the built class.
|
|
2009
|
+
def parse_object
|
|
2010
|
+
Parse::Object.build(self)
|
|
2011
|
+
end
|
|
2012
|
+
end
|
|
2013
|
+
|
|
2014
|
+
class Array
|
|
2015
|
+
# This helper method selects or converts all objects in an array that are either inherit from
|
|
2016
|
+
# Parse::Pointer or are a JSON Parse hash. If it is a hash, a Pare::Object will be built from it
|
|
2017
|
+
# if it constrains the proper fields. Non-convertible objects will be removed.
|
|
2018
|
+
#
|
|
2019
|
+
# When +className+ is provided by the caller, it is treated as authoritative
|
|
2020
|
+
# — incoming hash +className+ values are ignored. This blocks attacker-
|
|
2021
|
+
# controlled type confusion when this helper is invoked from typed
|
|
2022
|
+
# associations (+has_many+, +belongs_to+) that already know the expected
|
|
2023
|
+
# class.
|
|
2024
|
+
#
|
|
2025
|
+
# When +className+ is +nil+ (caller is doing untyped array conversion),
|
|
2026
|
+
# the helper falls back to the hash-supplied className for compatibility
|
|
2027
|
+
# with raw JSON deserialization callers.
|
|
2028
|
+
#
|
|
2029
|
+
# @param className [String, nil] the authoritative Parse class name.
|
|
2030
|
+
# @return [Array<Parse::Object>] an array of Parse::Object subclasses.
|
|
2031
|
+
def parse_objects(className = nil)
|
|
2032
|
+
f = Parse::Model::KEY_CLASS_NAME
|
|
2033
|
+
map do |m|
|
|
2034
|
+
next m if m.is_a?(Parse::Pointer)
|
|
2035
|
+
if m.is_a?(Hash)
|
|
2036
|
+
resolved = if className
|
|
2037
|
+
# Caller knows the type; warn on mismatch but always
|
|
2038
|
+
# use the declared className.
|
|
2039
|
+
incoming = m[f] || m[:className]
|
|
2040
|
+
if incoming && incoming != className
|
|
2041
|
+
warn "[Parse::Array#parse_objects] expected className=#{className.inspect}, ignoring incoming className=#{incoming.inspect}"
|
|
2042
|
+
end
|
|
2043
|
+
className
|
|
2044
|
+
else
|
|
2045
|
+
m[f] || m[:className]
|
|
2046
|
+
end
|
|
2047
|
+
next Parse::Object.build(m, resolved) if resolved
|
|
2048
|
+
end
|
|
2049
|
+
nil
|
|
2050
|
+
end.compact
|
|
2051
|
+
end
|
|
2052
|
+
|
|
2053
|
+
# @return [Array<String>] an array of objectIds for all objects that are Parse::Objects.
|
|
2054
|
+
def parse_ids
|
|
2055
|
+
parse_objects.map(&:id)
|
|
2056
|
+
end
|
|
2057
|
+
end
|
|
2058
|
+
|
|
2059
|
+
# Load all the core classes.
|
|
2060
|
+
require_relative "classes/audience"
|
|
2061
|
+
require_relative "classes/installation"
|
|
2062
|
+
require_relative "classes/job_schedule"
|
|
2063
|
+
require_relative "classes/job_status"
|
|
2064
|
+
require_relative "classes/product"
|
|
2065
|
+
require_relative "classes/push_status"
|
|
2066
|
+
require_relative "classes/role"
|
|
2067
|
+
require_relative "classes/session"
|
|
2068
|
+
require_relative "classes/user"
|