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,566 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "time"
|
|
5
|
+
require "parallel"
|
|
6
|
+
|
|
7
|
+
module Parse
|
|
8
|
+
# Combines a set of core functionality for {Parse::Object} and its subclasses.
|
|
9
|
+
module Core
|
|
10
|
+
# Defines the record fetching interface for instances of Parse::Object.
|
|
11
|
+
module Fetching
|
|
12
|
+
# Returns a thread-safe mutex for fetch operations.
|
|
13
|
+
# Each instance gets its own mutex to prevent concurrent fetch operations
|
|
14
|
+
# on the same object from causing race conditions.
|
|
15
|
+
# @return [Mutex] the mutex used for thread-safe fetching
|
|
16
|
+
# @!visibility private
|
|
17
|
+
def fetch_mutex
|
|
18
|
+
@fetch_mutex ||= Mutex.new
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Non-serializable instance variables that should be excluded from Marshal.
|
|
22
|
+
# - @fetch_mutex: Mutex objects cannot be marshalled
|
|
23
|
+
# - @client: HTTP client objects contain non-serializable connections
|
|
24
|
+
NON_SERIALIZABLE_IVARS = [:@fetch_mutex, :@client].freeze
|
|
25
|
+
|
|
26
|
+
# Custom marshal serialization to exclude non-serializable instance variables.
|
|
27
|
+
# @return [Hash] instance variables suitable for Marshal serialization
|
|
28
|
+
# @!visibility private
|
|
29
|
+
def marshal_dump
|
|
30
|
+
instance_variables.each_with_object({}) do |var, hash|
|
|
31
|
+
next if NON_SERIALIZABLE_IVARS.include?(var)
|
|
32
|
+
hash[var] = instance_variable_get(var)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Custom marshal deserialization to restore instance variables.
|
|
37
|
+
# @param data [Hash] the serialized instance variables
|
|
38
|
+
# @!visibility private
|
|
39
|
+
def marshal_load(data)
|
|
40
|
+
data.each do |var, value|
|
|
41
|
+
instance_variable_set(var, value)
|
|
42
|
+
end
|
|
43
|
+
# @fetch_mutex will be lazily initialized when needed
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Force fetches and updates the current object with the data contained in the Parse collection.
|
|
47
|
+
# The changes applied to the object are not dirty tracked.
|
|
48
|
+
# By default, bypasses cache reads but updates the cache with fresh data (write-only mode).
|
|
49
|
+
# This ensures you always get fresh data while keeping the cache updated for future reads.
|
|
50
|
+
# @param keys [Array<Symbol, String>, nil] optional list of fields to fetch (partial fetch).
|
|
51
|
+
# If provided, only these fields will be fetched and the object will be marked as partially fetched.
|
|
52
|
+
# Use dot notation for nested fields (e.g., "author.name") - Parse automatically resolves the pointer.
|
|
53
|
+
# @param includes [Array<String>, nil] optional list of pointer fields to resolve as FULL objects.
|
|
54
|
+
# Only needed when you want the complete nested object without field restrictions.
|
|
55
|
+
# @param preserve_changes [Boolean] if true, re-apply local dirty values to fetched fields.
|
|
56
|
+
# By default (false), fetched fields accept server values and local changes are discarded.
|
|
57
|
+
# Unfetched fields always preserve their dirty state regardless of this setting.
|
|
58
|
+
# @param opts [Hash] a set of options to pass to the client request.
|
|
59
|
+
# @option opts [Boolean, Symbol] :cache (:write_only) caching mode:
|
|
60
|
+
# - :write_only (default) - skip cache read, but update cache with fresh data
|
|
61
|
+
# - true - read from and write to cache
|
|
62
|
+
# - false - completely bypass cache (no read or write)
|
|
63
|
+
# @return [self] the current object, useful for chaining.
|
|
64
|
+
# @example Full fetch (updates cache but doesn't read from it)
|
|
65
|
+
# post.fetch!
|
|
66
|
+
# @example Fetch with full caching (read and write)
|
|
67
|
+
# post.fetch!(cache: true)
|
|
68
|
+
# @example Fetch completely bypassing cache
|
|
69
|
+
# post.fetch!(cache: false)
|
|
70
|
+
# @example Partial fetch with specific keys
|
|
71
|
+
# post.fetch!(keys: [:title, :content])
|
|
72
|
+
# @example Partial fetch with nested fields (pointer auto-resolved)
|
|
73
|
+
# post.fetch!(keys: ["title", "author.name", "author.email"])
|
|
74
|
+
# @example Full nested object (includes required for full resolution)
|
|
75
|
+
# post.fetch!(keys: [:title, :author], includes: [:author])
|
|
76
|
+
# @example Preserve local changes during fetch
|
|
77
|
+
# post.fetch!(keys: [:title], preserve_changes: true)
|
|
78
|
+
def fetch!(keys: nil, includes: nil, preserve_changes: false, **opts)
|
|
79
|
+
# Default to write-only cache mode - fetch fresh data but update cache
|
|
80
|
+
# This can be disabled globally with Parse.cache_write_on_fetch = false
|
|
81
|
+
unless opts.key?(:cache)
|
|
82
|
+
opts[:cache] = Parse.cache_write_on_fetch ? :write_only : false
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Normalize keys and includes arrays once at the start for performance
|
|
86
|
+
keys_array = keys.present? ? Array(keys) : nil
|
|
87
|
+
includes_array = includes.present? ? Array(includes) : nil
|
|
88
|
+
|
|
89
|
+
# Build formatted keys once (reused for query and tracking)
|
|
90
|
+
formatted_keys = keys_array&.map { |k| Parse::Query.format_field(k) }
|
|
91
|
+
|
|
92
|
+
# Validate keys against model fields if validation is enabled
|
|
93
|
+
# Skip validation if warnings are disabled (nothing to report)
|
|
94
|
+
if keys_array && Parse.validate_query_keys && Parse.warn_on_query_issues
|
|
95
|
+
validate_fetch_keys(keys_array)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Build query parameters for partial fetch
|
|
99
|
+
query = {}
|
|
100
|
+
query[:keys] = formatted_keys.join(",") if formatted_keys
|
|
101
|
+
query[:include] = includes_array.map(&:to_s).join(",") if includes_array
|
|
102
|
+
|
|
103
|
+
response = client.fetch_object(parse_class, id, query: query.presence, **opts)
|
|
104
|
+
if response.error?
|
|
105
|
+
puts "[Fetch Error] #{response.code}: #{response.error}"
|
|
106
|
+
# Raise appropriate error based on response code
|
|
107
|
+
case response.code
|
|
108
|
+
when 101 # Object not found
|
|
109
|
+
raise Parse::Error::ProtocolError, "Object not found"
|
|
110
|
+
else
|
|
111
|
+
raise Parse::Error::ProtocolError, response.error
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Handle empty results gracefully - clear the object rather than error
|
|
116
|
+
result = response.result
|
|
117
|
+
if result.nil? || (result.is_a?(Array) && result.empty?)
|
|
118
|
+
# Mark object as deleted and clear the ID
|
|
119
|
+
@_deleted = true
|
|
120
|
+
@id = nil
|
|
121
|
+
clear_changes!
|
|
122
|
+
return self
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Handle case where result is an Array (e.g., batch operations or certain API responses)
|
|
126
|
+
# This is unexpected for single-object fetch but handled defensively
|
|
127
|
+
if result.is_a?(Array)
|
|
128
|
+
warn "[Parse::Fetch] Unexpected array response for fetch_object (id: #{id}). This may indicate an API issue."
|
|
129
|
+
result = result.find { |r| r.is_a?(Hash) && (r["objectId"] == id || r["id"] == id) }
|
|
130
|
+
if result.nil?
|
|
131
|
+
warn "[Parse::Fetch] Object #{id} not found in array response - marking as deleted"
|
|
132
|
+
@_deleted = true
|
|
133
|
+
@id = nil
|
|
134
|
+
clear_changes!
|
|
135
|
+
return self
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# If we successfully fetched data, ensure the object is not marked as deleted
|
|
140
|
+
@_deleted = false
|
|
141
|
+
|
|
142
|
+
# Capture dirty fields and their local values BEFORE applying server data
|
|
143
|
+
dirty_fields = {}
|
|
144
|
+
if respond_to?(:changed)
|
|
145
|
+
begin
|
|
146
|
+
changed_attrs = changed
|
|
147
|
+
if changed_attrs.respond_to?(:each)
|
|
148
|
+
changed_attrs.each do |attr|
|
|
149
|
+
# Only capture if object responds to the attribute getter
|
|
150
|
+
if respond_to?(attr)
|
|
151
|
+
begin
|
|
152
|
+
dirty_fields[attr.to_sym] = send(attr)
|
|
153
|
+
rescue NoMethodError => e
|
|
154
|
+
# Skip this attribute if its getter raises NoMethodError
|
|
155
|
+
warn "[Parse::Fetch] Skipping dirty field :#{attr}: #{e.message}"
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
rescue NoMethodError => e
|
|
161
|
+
# Handle ActiveModel 8.x compatibility issues where `changed` method itself fails
|
|
162
|
+
# due to unexpected state (e.g., after transaction rollback)
|
|
163
|
+
warn "[Parse::Fetch] Warning: changed tracking unavailable: #{e.message}"
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Determine if this is a partial fetch
|
|
168
|
+
is_partial_fetch = keys_array.present?
|
|
169
|
+
|
|
170
|
+
if is_partial_fetch
|
|
171
|
+
# Build the new fetched keys list (top-level keys only, without nested paths)
|
|
172
|
+
# Reuse formatted_keys instead of calling format_field again
|
|
173
|
+
new_keys = formatted_keys.map { |k| k.split(".").first.to_sym }
|
|
174
|
+
new_keys << :id unless new_keys.include?(:id)
|
|
175
|
+
new_keys << :objectId unless new_keys.include?(:objectId)
|
|
176
|
+
new_keys.uniq!
|
|
177
|
+
|
|
178
|
+
# If already selectively fetched, merge with existing keys
|
|
179
|
+
if has_selective_keys?
|
|
180
|
+
@_fetched_keys = (@_fetched_keys + new_keys).uniq
|
|
181
|
+
else
|
|
182
|
+
@_fetched_keys = new_keys
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Parse keys with dot notation into nested fetched keys and merge
|
|
186
|
+
new_nested_keys = Parse::Query.parse_keys_to_nested_keys(keys_array)
|
|
187
|
+
if new_nested_keys.present?
|
|
188
|
+
if @_nested_fetched_keys.present?
|
|
189
|
+
# Merge nested keys
|
|
190
|
+
new_nested_keys.each do |field, nested|
|
|
191
|
+
@_nested_fetched_keys[field] ||= []
|
|
192
|
+
@_nested_fetched_keys[field] = (@_nested_fetched_keys[field] + nested).uniq
|
|
193
|
+
end
|
|
194
|
+
else
|
|
195
|
+
@_nested_fetched_keys = new_nested_keys
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
else
|
|
199
|
+
# Full fetch - clear partial fetch tracking
|
|
200
|
+
@_fetched_keys = nil
|
|
201
|
+
@_nested_fetched_keys = nil
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Apply attributes from server (only keys in result get updated)
|
|
205
|
+
apply_attributes!(result, dirty_track: false)
|
|
206
|
+
|
|
207
|
+
begin
|
|
208
|
+
clear_changes!
|
|
209
|
+
rescue => e
|
|
210
|
+
# Log the error for debugging purposes
|
|
211
|
+
warn "[Parse::Fetch] Warning: clear_changes! failed: #{e.class}: #{e.message}"
|
|
212
|
+
# If clear_changes! fails, manually reset change tracking
|
|
213
|
+
@changed_attributes = {} if instance_variable_defined?(:@changed_attributes)
|
|
214
|
+
@mutations_from_database = nil if instance_variable_defined?(:@mutations_from_database)
|
|
215
|
+
@mutations_before_last_save = nil if instance_variable_defined?(:@mutations_before_last_save)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Handle previously dirty fields based on preserve_changes setting
|
|
219
|
+
dirty_fields.each do |attr, local_value|
|
|
220
|
+
attr_sym = attr.to_sym
|
|
221
|
+
|
|
222
|
+
# Skip base fields (id, objectId, created_at, updated_at) - they should always accept server values
|
|
223
|
+
next if Parse::Properties::BASE_KEYS.include?(attr_sym)
|
|
224
|
+
|
|
225
|
+
# Determine the remote field name for this attribute
|
|
226
|
+
remote_field = self.field_map[attr_sym]&.to_s || attr.to_s
|
|
227
|
+
|
|
228
|
+
# Check if this field was in the server response (i.e., was fetched)
|
|
229
|
+
field_in_response = result.key?(remote_field) || result.key?(attr.to_s)
|
|
230
|
+
|
|
231
|
+
if field_in_response
|
|
232
|
+
# Field was fetched from server
|
|
233
|
+
current_server_value = send(attr)
|
|
234
|
+
|
|
235
|
+
if preserve_changes
|
|
236
|
+
# Re-apply local value - ActiveModel will mark dirty if value differs
|
|
237
|
+
setter = "#{attr}="
|
|
238
|
+
send(setter, local_value) if respond_to?(setter)
|
|
239
|
+
else
|
|
240
|
+
# Default behavior: accept server value, warn if local value was different
|
|
241
|
+
if current_server_value != local_value
|
|
242
|
+
puts "[Parse::Fetch] Field :#{attr} had unsaved changes that were discarded (local: #{local_value.inspect}, server: #{current_server_value.inspect}). Use preserve_changes: true to keep local changes."
|
|
243
|
+
end
|
|
244
|
+
# Server value is already applied, nothing more to do
|
|
245
|
+
end
|
|
246
|
+
else
|
|
247
|
+
# Field was NOT fetched - always preserve dirty state
|
|
248
|
+
# Use will_change! to mark as dirty since clear_changes! cleared the flag
|
|
249
|
+
will_change_method = "#{attr}_will_change!"
|
|
250
|
+
send(will_change_method) if respond_to?(will_change_method)
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
self
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Fetches the object with explicit caching enabled.
|
|
258
|
+
# This is a convenience method that calls fetch! with cache: true.
|
|
259
|
+
# Use this when you want to leverage cached responses for better performance.
|
|
260
|
+
# @param keys [Array<Symbol, String>, nil] optional list of fields to fetch (partial fetch).
|
|
261
|
+
# @param includes [Array<String>, nil] optional list of pointer fields to resolve.
|
|
262
|
+
# @param preserve_changes [Boolean] if true, re-apply local dirty values to fetched fields.
|
|
263
|
+
# @param opts [Hash] additional options to pass to the client request.
|
|
264
|
+
# @return [self] the current object, useful for chaining.
|
|
265
|
+
# @example Fetch with caching
|
|
266
|
+
# post.fetch_cache!
|
|
267
|
+
# @example Partial fetch with caching
|
|
268
|
+
# post.fetch_cache!(keys: [:title, :content])
|
|
269
|
+
# @see #fetch!
|
|
270
|
+
def fetch_cache!(keys: nil, includes: nil, preserve_changes: false, **opts)
|
|
271
|
+
fetch!(keys: keys, includes: includes, preserve_changes: preserve_changes, cache: true, **opts)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Fetches the object from the Parse data store. Unlike fetchIfNeeded, this always
|
|
275
|
+
# fetches from the server and updates the local object with fresh data.
|
|
276
|
+
# @overload fetch
|
|
277
|
+
# Full fetch - fetches all fields
|
|
278
|
+
# @return [self] the current object with updated data
|
|
279
|
+
# @overload fetch(return_object)
|
|
280
|
+
# Legacy signature for backward compatibility.
|
|
281
|
+
# @param return_object [Boolean] if true returns self, if false returns raw JSON
|
|
282
|
+
# @return [self, Hash] the object or raw JSON data
|
|
283
|
+
# @deprecated Use fetch or fetch_json instead
|
|
284
|
+
# @overload fetch(keys:, includes:, preserve_changes:)
|
|
285
|
+
# Partial fetch - fetches only specified fields
|
|
286
|
+
# @param keys [Array<Symbol, String>, nil] optional list of fields to fetch (partial fetch).
|
|
287
|
+
# If provided, only these fields will be fetched and the object will be marked as partially fetched.
|
|
288
|
+
# Use dot notation for nested fields (e.g., "author.name") - pointer auto-resolved.
|
|
289
|
+
# @param includes [Array<String>, nil] optional list of pointer fields to resolve as FULL objects.
|
|
290
|
+
# Only needed when you want the complete nested object without field restrictions.
|
|
291
|
+
# @param preserve_changes [Boolean] if true, re-apply local dirty values to fetched fields.
|
|
292
|
+
# By default (false), fetched fields accept server values.
|
|
293
|
+
# @return [self] the current object with updated data.
|
|
294
|
+
# @example Full fetch
|
|
295
|
+
# post.fetch
|
|
296
|
+
# @example Partial fetch with specific keys
|
|
297
|
+
# post.fetch(keys: [:title, :content])
|
|
298
|
+
# @example Partial fetch with nested fields (pointer auto-resolved)
|
|
299
|
+
# post.fetch(keys: ["title", "author.name", "author.email"])
|
|
300
|
+
# @example Preserve local changes during fetch
|
|
301
|
+
# post.fetch(keys: [:title], preserve_changes: true)
|
|
302
|
+
def fetch(return_object = nil, keys: nil, includes: nil, preserve_changes: false)
|
|
303
|
+
# Handle legacy signature: fetch(true) or fetch(false)
|
|
304
|
+
if return_object == false
|
|
305
|
+
return fetch_json(keys: keys, includes: includes)
|
|
306
|
+
end
|
|
307
|
+
# For fetch(), fetch(true), or fetch(keys: ..., includes: ..., preserve_changes: ...)
|
|
308
|
+
fetch!(keys: keys, includes: includes, preserve_changes: preserve_changes)
|
|
309
|
+
self
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# Returns raw JSON data from the server without updating the current object.
|
|
313
|
+
# @param keys [Array<Symbol, String>, nil] optional list of fields to fetch.
|
|
314
|
+
# @param includes [Array<String>, nil] optional list of pointer fields to expand.
|
|
315
|
+
# @return [Hash, nil] the raw JSON data or nil if error.
|
|
316
|
+
def fetch_json(keys: nil, includes: nil)
|
|
317
|
+
query = {}
|
|
318
|
+
if keys.present?
|
|
319
|
+
keys_array = Array(keys).map { |k| Parse::Query.format_field(k) }
|
|
320
|
+
query[:keys] = keys_array.join(",")
|
|
321
|
+
end
|
|
322
|
+
if includes.present?
|
|
323
|
+
includes_array = Array(includes).map(&:to_s)
|
|
324
|
+
query[:include] = includes_array.join(",")
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
response = client.fetch_object(parse_class, id, query: query.presence)
|
|
328
|
+
return nil if response.error?
|
|
329
|
+
response.result
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# Fetches the Parse object from the data store and returns a Parse::Object instance.
|
|
333
|
+
# This is a convenience method that calls fetch.
|
|
334
|
+
# @return [Parse::Object] the fetched Parse::Object (self if already fetched).
|
|
335
|
+
def fetch_object
|
|
336
|
+
fetch
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# Validates includes against keys for fetch operations, printing debug warnings for:
|
|
340
|
+
# 1. Non-pointer fields that are included (unnecessary include)
|
|
341
|
+
# 2. Pointer fields that are included but also have subfield keys (redundant keys)
|
|
342
|
+
# Skips validation for includes with dot notation (internal references).
|
|
343
|
+
# Can be disabled by setting Parse.warn_on_query_issues = false
|
|
344
|
+
# @param keys [Array] the keys array
|
|
345
|
+
# @param includes [Array] the includes array
|
|
346
|
+
# @!visibility private
|
|
347
|
+
def validate_fetch_includes_vs_keys(keys, includes)
|
|
348
|
+
return unless Parse.warn_on_query_issues
|
|
349
|
+
return if includes.nil? || includes.empty?
|
|
350
|
+
|
|
351
|
+
keys_array = Array(keys).map(&:to_s)
|
|
352
|
+
fields = self.class.respond_to?(:fields) ? self.class.fields : {}
|
|
353
|
+
|
|
354
|
+
Array(includes).each do |inc|
|
|
355
|
+
inc_str = inc.to_s
|
|
356
|
+
|
|
357
|
+
# Skip includes with dots - these are internal references (e.g., "project.owner")
|
|
358
|
+
next if inc_str.include?(".")
|
|
359
|
+
|
|
360
|
+
inc_sym = inc_str.to_sym
|
|
361
|
+
field_type = fields[inc_sym]
|
|
362
|
+
|
|
363
|
+
# Check if the field is a pointer or relation type
|
|
364
|
+
is_object_field = [:pointer, :relation].include?(field_type)
|
|
365
|
+
|
|
366
|
+
if !is_object_field && field_type.present?
|
|
367
|
+
# Warn: non-object field doesn't need to be included
|
|
368
|
+
puts "[Parse::Fetch] Warning: '#{inc_str}' is a #{field_type} field, not a pointer/relation - it does not need to be included (silence with Parse.warn_on_query_issues = false)"
|
|
369
|
+
elsif is_object_field
|
|
370
|
+
# Check if there are keys with dot notation for this field
|
|
371
|
+
subfield_keys = keys_array.select { |k| k.start_with?("#{inc_str}.") }
|
|
372
|
+
|
|
373
|
+
if subfield_keys.any?
|
|
374
|
+
# Warn: including the full object makes subfield keys unnecessary
|
|
375
|
+
puts "[Parse::Fetch] Warning: including '#{inc_str}' returns the full object - keys #{subfield_keys.inspect} are unnecessary (silence with Parse.warn_on_query_issues = false)"
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
private :validate_fetch_includes_vs_keys
|
|
382
|
+
|
|
383
|
+
# Validates that fetch keys match defined properties on the model.
|
|
384
|
+
# Warns about unknown keys that don't correspond to any field.
|
|
385
|
+
# Skips validation for base keys (objectId, createdAt, etc.) and nested keys.
|
|
386
|
+
# @param keys [Array] the keys array to validate
|
|
387
|
+
# @!visibility private
|
|
388
|
+
def validate_fetch_keys(keys)
|
|
389
|
+
return unless self.class.respond_to?(:fields)
|
|
390
|
+
|
|
391
|
+
model_fields = self.class.fields
|
|
392
|
+
unknown_keys = []
|
|
393
|
+
|
|
394
|
+
keys.each do |key|
|
|
395
|
+
key_str = key.to_s
|
|
396
|
+
# Extract top-level field (before any dot notation)
|
|
397
|
+
top_level_key = key_str.split(".").first.to_sym
|
|
398
|
+
|
|
399
|
+
# Skip base keys (objectId, createdAt, updatedAt, ACL)
|
|
400
|
+
next if Parse::Properties::BASE_KEYS.include?(top_level_key)
|
|
401
|
+
next if [:acl, :ACL, :objectId].include?(top_level_key)
|
|
402
|
+
|
|
403
|
+
# Check if field exists on the model
|
|
404
|
+
unless model_fields.key?(top_level_key)
|
|
405
|
+
unknown_keys << top_level_key
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
if unknown_keys.any?
|
|
410
|
+
unknown_keys.uniq!
|
|
411
|
+
puts "[Parse::Fetch] Warning: unknown keys #{unknown_keys.inspect} for #{self.class.name}. " \
|
|
412
|
+
"These fields are not defined on the model. (silence with Parse.validate_query_keys = false)"
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
private :validate_fetch_keys
|
|
417
|
+
|
|
418
|
+
# Autofetches the object based on a key that is not part {Parse::Properties::BASE_KEYS}.
|
|
419
|
+
# If the key is not a Parse standard key, and the current object is in a
|
|
420
|
+
# Pointer state or was selectively fetched, then fetch the data related to
|
|
421
|
+
# this record from the Parse data store.
|
|
422
|
+
# Uses a mutex for thread safety to prevent race conditions in multi-threaded contexts.
|
|
423
|
+
# @param key [String] the name of the attribute being accessed.
|
|
424
|
+
# @param source_info [Hash] optional info about where this autofetch was triggered from
|
|
425
|
+
# (used for N+1 detection with belongs_to associations)
|
|
426
|
+
# @return [Boolean]
|
|
427
|
+
def autofetch!(key, source_info: nil)
|
|
428
|
+
key = key.to_sym
|
|
429
|
+
|
|
430
|
+
# Autofetch if object is a pointer OR was selectively fetched
|
|
431
|
+
# Skip if autofetch is disabled for this instance
|
|
432
|
+
needs_fetch = pointer? || has_selective_keys?
|
|
433
|
+
return unless needs_fetch &&
|
|
434
|
+
!autofetch_disabled? &&
|
|
435
|
+
key != :acl &&
|
|
436
|
+
!Parse::Properties::BASE_KEYS.include?(key) &&
|
|
437
|
+
respond_to?(:fetch)
|
|
438
|
+
|
|
439
|
+
# Capture caller stack BEFORE mutex for better error tracebacks
|
|
440
|
+
# Filter out internal parse-stack frames to show where user code accessed the field
|
|
441
|
+
caller_stack = caller.reject { |frame| frame.include?("/lib/parse/") }
|
|
442
|
+
|
|
443
|
+
# Use mutex for thread-safe check-and-fetch pattern
|
|
444
|
+
fetch_mutex.synchronize do
|
|
445
|
+
# Double-check inside mutex (another thread may have fetched)
|
|
446
|
+
return if !pointer? && !has_selective_keys?
|
|
447
|
+
|
|
448
|
+
is_pointer_fetch = pointer?
|
|
449
|
+
|
|
450
|
+
# Track for N+1 detection if enabled
|
|
451
|
+
if is_pointer_fetch && Parse.warn_on_n_plus_one
|
|
452
|
+
# Check for source info in the registry (set by belongs_to getter)
|
|
453
|
+
n_plus_one_source = Parse::NPlusOneDetector.lookup_source(self)
|
|
454
|
+
source_class = source_info&.dig(:source_class) || n_plus_one_source&.dig(:source_class) || self.class.name
|
|
455
|
+
association = source_info&.dig(:association) || n_plus_one_source&.dig(:association) || key
|
|
456
|
+
Parse::NPlusOneDetector.track_autofetch(
|
|
457
|
+
source_class: source_class,
|
|
458
|
+
association: association,
|
|
459
|
+
target_class: self.class.name,
|
|
460
|
+
object_id: id,
|
|
461
|
+
)
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
# If autofetch_raise_on_missing_keys is enabled, raise an error instead of fetching
|
|
465
|
+
# This helps developers identify where they need to add keys to their queries
|
|
466
|
+
if Parse.autofetch_raise_on_missing_keys
|
|
467
|
+
error = Parse::AutofetchTriggeredError.new(self.class, id, key, is_pointer: is_pointer_fetch)
|
|
468
|
+
error.set_backtrace(caller_stack)
|
|
469
|
+
raise error
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
# Log info about autofetch being triggered (conditional on warn_on_query_issues)
|
|
473
|
+
if Parse.warn_on_query_issues
|
|
474
|
+
if is_pointer_fetch
|
|
475
|
+
puts "[Parse::Autofetch] Fetching #{self.class}##{id} - pointer accessed field :#{key} (silence with Parse.warn_on_query_issues = false)"
|
|
476
|
+
else
|
|
477
|
+
puts "[Parse::Autofetch] Fetching #{self.class}##{id} - field :#{key} was not included in partial fetch (silence with Parse.warn_on_query_issues = false)"
|
|
478
|
+
end
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
# Autofetch always preserves changes - it's an implicit background operation
|
|
482
|
+
# that shouldn't discard user modifications
|
|
483
|
+
send :fetch, keys: nil, includes: nil, preserve_changes: true
|
|
484
|
+
end
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
# Prepares object for dirty tracking by fetching if needed.
|
|
488
|
+
# Must be called BEFORE will_change! to prevent autofetch from wiping dirty state.
|
|
489
|
+
#
|
|
490
|
+
# When will_change! captures the old value by calling the getter, it may trigger
|
|
491
|
+
# autofetch if the object is a pointer. That autofetch calls clear_changes! which
|
|
492
|
+
# wipes the dirty tracking state will_change! is trying to set up.
|
|
493
|
+
#
|
|
494
|
+
# By fetching first, the object is no longer a pointer, so will_change! can
|
|
495
|
+
# proceed without triggering another fetch.
|
|
496
|
+
#
|
|
497
|
+
# For selective fetch objects, this also marks the field as fetched to prevent
|
|
498
|
+
# autofetch during will_change!'s getter call.
|
|
499
|
+
#
|
|
500
|
+
# @param key [Symbol] the name of the attribute being set
|
|
501
|
+
# @return [void]
|
|
502
|
+
def prepare_for_dirty_tracking!(key)
|
|
503
|
+
# Fetch before will_change! to prevent clear_changes! interference
|
|
504
|
+
if pointer? && !autofetch_disabled?
|
|
505
|
+
autofetch!(key)
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
# Mark selective fetch fields as fetched to prevent autofetch during will_change!
|
|
509
|
+
if has_selective_keys? && !field_was_fetched?(key)
|
|
510
|
+
@_fetched_keys ||= []
|
|
511
|
+
@_fetched_keys << key unless @_fetched_keys.include?(key)
|
|
512
|
+
end
|
|
513
|
+
end
|
|
514
|
+
end
|
|
515
|
+
end
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
class Array
|
|
519
|
+
|
|
520
|
+
# Perform a threaded each iteration on a set of array items.
|
|
521
|
+
# @param threads [Integer] the maximum number of threads to spawn/
|
|
522
|
+
# @yield the block for the each iteration.
|
|
523
|
+
# @return [self]
|
|
524
|
+
# @see Array#each
|
|
525
|
+
# @see https://github.com/grosser/parallel Parallel
|
|
526
|
+
def threaded_each(threads = 2, &block)
|
|
527
|
+
Parallel.each(self, { in_threads: threads }, &block)
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
# Perform a threaded map operation on a set of array items.
|
|
531
|
+
# @param threads [Integer] the maximum number of threads to spawn
|
|
532
|
+
# @yield the block for the map iteration.
|
|
533
|
+
# @return [Array] the resultant array from the map.
|
|
534
|
+
# @see Array#map
|
|
535
|
+
# @see https://github.com/grosser/parallel Parallel
|
|
536
|
+
def threaded_map(threads = 2, &block)
|
|
537
|
+
Parallel.map(self, { in_threads: threads }, &block)
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
# Fetches all the objects in the array even if they are not in a Pointer state.
|
|
541
|
+
# @param lookup [Symbol] The methodology to use for HTTP requests. Use :parallel
|
|
542
|
+
# to fetch all objects in parallel HTTP requests. Set to anything else to
|
|
543
|
+
# perform requests serially.
|
|
544
|
+
# @return [Array<Parse::Object>] an array of fetched Parse::Objects.
|
|
545
|
+
# @see Array#fetch_objects
|
|
546
|
+
def fetch_objects!(lookup = :parallel)
|
|
547
|
+
# this gets all valid parse objects from the array
|
|
548
|
+
items = valid_parse_objects
|
|
549
|
+
lookup == :parallel ? items.threaded_each(2, &:fetch!) : items.each(&:fetch!)
|
|
550
|
+
#self.replace items
|
|
551
|
+
self #return for chaining.
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
# Fetches all the objects in the array that are in Pointer state.
|
|
555
|
+
# @param lookup [Symbol] The methodology to use for HTTP requests. Use :parallel
|
|
556
|
+
# to fetch all objects in parallel HTTP requests. Set to anything else to
|
|
557
|
+
# perform requests serially.
|
|
558
|
+
# @return [Array<Parse::Object>] an array of fetched Parse::Objects.
|
|
559
|
+
# @see Array#fetch_objects!
|
|
560
|
+
def fetch_objects(lookup = :parallel)
|
|
561
|
+
items = valid_parse_objects
|
|
562
|
+
lookup == :parallel ? items.threaded_each(2, &:fetch) : items.each(&:fetch)
|
|
563
|
+
#self.replace items
|
|
564
|
+
self
|
|
565
|
+
end
|
|
566
|
+
end
|