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.
Files changed (178) hide show
  1. checksums.yaml +7 -0
  2. data/.bundle/config +2 -0
  3. data/.env.sample +112 -0
  4. data/.env.test +10 -0
  5. data/.github/workflows/ruby.yml +36 -0
  6. data/.gitignore +49 -0
  7. data/.ruby-version +1 -0
  8. data/.solargraph.yml +22 -0
  9. data/CHANGELOG.md +5816 -0
  10. data/Gemfile +30 -0
  11. data/Gemfile.lock +175 -0
  12. data/LICENSE.txt +23 -0
  13. data/Makefile +63 -0
  14. data/README.md +5655 -0
  15. data/Rakefile +573 -0
  16. data/bin/console +38 -0
  17. data/bin/parse-console +136 -0
  18. data/bin/server +17 -0
  19. data/bin/setup +7 -0
  20. data/config/parse-config.json +12 -0
  21. data/docs/TEST_SERVER.md +271 -0
  22. data/docs/_config.yml +1 -0
  23. data/docs/mcp_guide.md +3484 -0
  24. data/docs/mongodb_direct_guide.md +1348 -0
  25. data/docs/mongodb_index_optimization_guide.md +631 -0
  26. data/examples/transaction_example.rb +219 -0
  27. data/lib/parse/acl_scope.rb +728 -0
  28. data/lib/parse/agent/cancellation_token.rb +80 -0
  29. data/lib/parse/agent/constraint_translator.rb +480 -0
  30. data/lib/parse/agent/describe.rb +420 -0
  31. data/lib/parse/agent/errors.rb +133 -0
  32. data/lib/parse/agent/mcp_client.rb +557 -0
  33. data/lib/parse/agent/mcp_dispatcher.rb +1023 -0
  34. data/lib/parse/agent/mcp_rack_app.rb +1143 -0
  35. data/lib/parse/agent/mcp_server.rb +376 -0
  36. data/lib/parse/agent/metadata_audit.rb +259 -0
  37. data/lib/parse/agent/metadata_dsl.rb +733 -0
  38. data/lib/parse/agent/metadata_registry.rb +794 -0
  39. data/lib/parse/agent/pipeline_validator.rb +82 -0
  40. data/lib/parse/agent/prompts.rb +351 -0
  41. data/lib/parse/agent/rate_limiter.rb +158 -0
  42. data/lib/parse/agent/relation_graph.rb +162 -0
  43. data/lib/parse/agent/result_formatter.rb +453 -0
  44. data/lib/parse/agent/tools.rb +5489 -0
  45. data/lib/parse/agent.rb +3249 -0
  46. data/lib/parse/api/aggregate.rb +79 -0
  47. data/lib/parse/api/all.rb +26 -0
  48. data/lib/parse/api/analytics.rb +18 -0
  49. data/lib/parse/api/batch.rb +33 -0
  50. data/lib/parse/api/cloud_functions.rb +58 -0
  51. data/lib/parse/api/config.rb +125 -0
  52. data/lib/parse/api/files.rb +29 -0
  53. data/lib/parse/api/hooks.rb +117 -0
  54. data/lib/parse/api/objects.rb +146 -0
  55. data/lib/parse/api/path_segment.rb +75 -0
  56. data/lib/parse/api/push.rb +20 -0
  57. data/lib/parse/api/schema.rb +49 -0
  58. data/lib/parse/api/server.rb +50 -0
  59. data/lib/parse/api/sessions.rb +24 -0
  60. data/lib/parse/api/users.rb +250 -0
  61. data/lib/parse/atlas_search/index_manager.rb +353 -0
  62. data/lib/parse/atlas_search/result.rb +204 -0
  63. data/lib/parse/atlas_search/search_builder.rb +604 -0
  64. data/lib/parse/atlas_search/session.rb +253 -0
  65. data/lib/parse/atlas_search.rb +995 -0
  66. data/lib/parse/client/authentication.rb +97 -0
  67. data/lib/parse/client/batch.rb +234 -0
  68. data/lib/parse/client/body_builder.rb +240 -0
  69. data/lib/parse/client/caching.rb +203 -0
  70. data/lib/parse/client/logging.rb +293 -0
  71. data/lib/parse/client/profiling.rb +181 -0
  72. data/lib/parse/client/protocol.rb +91 -0
  73. data/lib/parse/client/request.rb +233 -0
  74. data/lib/parse/client/response.rb +208 -0
  75. data/lib/parse/client.rb +1104 -0
  76. data/lib/parse/clp_scope.rb +361 -0
  77. data/lib/parse/live_query/circuit_breaker.rb +256 -0
  78. data/lib/parse/live_query/client.rb +1001 -0
  79. data/lib/parse/live_query/configuration.rb +224 -0
  80. data/lib/parse/live_query/event.rb +115 -0
  81. data/lib/parse/live_query/event_queue.rb +272 -0
  82. data/lib/parse/live_query/health_monitor.rb +214 -0
  83. data/lib/parse/live_query/logging.rb +149 -0
  84. data/lib/parse/live_query/subscription.rb +294 -0
  85. data/lib/parse/live_query.rb +163 -0
  86. data/lib/parse/lookup_rewriter.rb +445 -0
  87. data/lib/parse/model/acl.rb +968 -0
  88. data/lib/parse/model/associations/belongs_to.rb +275 -0
  89. data/lib/parse/model/associations/collection_proxy.rb +435 -0
  90. data/lib/parse/model/associations/has_many.rb +597 -0
  91. data/lib/parse/model/associations/has_one.rb +158 -0
  92. data/lib/parse/model/associations/pointer_collection_proxy.rb +134 -0
  93. data/lib/parse/model/associations/relation_collection_proxy.rb +177 -0
  94. data/lib/parse/model/bytes.rb +62 -0
  95. data/lib/parse/model/classes/audience.rb +262 -0
  96. data/lib/parse/model/classes/installation.rb +363 -0
  97. data/lib/parse/model/classes/job_schedule.rb +153 -0
  98. data/lib/parse/model/classes/job_status.rb +264 -0
  99. data/lib/parse/model/classes/product.rb +75 -0
  100. data/lib/parse/model/classes/push_status.rb +263 -0
  101. data/lib/parse/model/classes/role.rb +751 -0
  102. data/lib/parse/model/classes/session.rb +201 -0
  103. data/lib/parse/model/classes/user.rb +943 -0
  104. data/lib/parse/model/clp.rb +544 -0
  105. data/lib/parse/model/core/actions.rb +1268 -0
  106. data/lib/parse/model/core/builder.rb +139 -0
  107. data/lib/parse/model/core/create_lock.rb +386 -0
  108. data/lib/parse/model/core/describe.rb +382 -0
  109. data/lib/parse/model/core/enhanced_change_tracking.rb +159 -0
  110. data/lib/parse/model/core/errors.rb +38 -0
  111. data/lib/parse/model/core/fetching.rb +566 -0
  112. data/lib/parse/model/core/field_guards.rb +220 -0
  113. data/lib/parse/model/core/indexing.rb +382 -0
  114. data/lib/parse/model/core/parse_reference.rb +407 -0
  115. data/lib/parse/model/core/properties.rb +809 -0
  116. data/lib/parse/model/core/querying.rb +491 -0
  117. data/lib/parse/model/core/schema.rb +202 -0
  118. data/lib/parse/model/core/search_indexing.rb +174 -0
  119. data/lib/parse/model/date.rb +88 -0
  120. data/lib/parse/model/email.rb +213 -0
  121. data/lib/parse/model/file.rb +527 -0
  122. data/lib/parse/model/geojson.rb +271 -0
  123. data/lib/parse/model/geopoint.rb +261 -0
  124. data/lib/parse/model/model.rb +260 -0
  125. data/lib/parse/model/object.rb +2068 -0
  126. data/lib/parse/model/phone.rb +520 -0
  127. data/lib/parse/model/pointer.rb +443 -0
  128. data/lib/parse/model/polygon.rb +406 -0
  129. data/lib/parse/model/push.rb +975 -0
  130. data/lib/parse/model/shortnames.rb +8 -0
  131. data/lib/parse/model/time_zone.rb +141 -0
  132. data/lib/parse/model/validations/uniqueness_validator.rb +97 -0
  133. data/lib/parse/model/validations.rb +96 -0
  134. data/lib/parse/mongodb.rb +2300 -0
  135. data/lib/parse/pipeline_security.rb +554 -0
  136. data/lib/parse/query/constraint.rb +198 -0
  137. data/lib/parse/query/constraints.rb +3279 -0
  138. data/lib/parse/query/cursor.rb +434 -0
  139. data/lib/parse/query/n_plus_one_detector.rb +445 -0
  140. data/lib/parse/query/operation.rb +104 -0
  141. data/lib/parse/query/ordering.rb +66 -0
  142. data/lib/parse/query.rb +7028 -0
  143. data/lib/parse/schema/index_migrator.rb +291 -0
  144. data/lib/parse/schema/search_index_migrator.rb +289 -0
  145. data/lib/parse/schema.rb +494 -0
  146. data/lib/parse/stack/generators/rails.rb +40 -0
  147. data/lib/parse/stack/generators/templates/model.erb +51 -0
  148. data/lib/parse/stack/generators/templates/model_installation.rb +4 -0
  149. data/lib/parse/stack/generators/templates/model_role.rb +4 -0
  150. data/lib/parse/stack/generators/templates/model_session.rb +4 -0
  151. data/lib/parse/stack/generators/templates/model_user.rb +11 -0
  152. data/lib/parse/stack/generators/templates/parse.rb +12 -0
  153. data/lib/parse/stack/generators/templates/webhooks.rb +10 -0
  154. data/lib/parse/stack/railtie.rb +18 -0
  155. data/lib/parse/stack/tasks.rb +563 -0
  156. data/lib/parse/stack/version.rb +11 -0
  157. data/lib/parse/stack.rb +455 -0
  158. data/lib/parse/two_factor_auth/user_extension.rb +449 -0
  159. data/lib/parse/two_factor_auth.rb +310 -0
  160. data/lib/parse/webhooks/payload.rb +360 -0
  161. data/lib/parse/webhooks/registration.rb +199 -0
  162. data/lib/parse/webhooks/replay_protection.rb +189 -0
  163. data/lib/parse/webhooks.rb +510 -0
  164. data/lib/parse-stack-next.rb +5 -0
  165. data/lib/parse-stack.rb +5 -0
  166. data/parse-stack-next.gemspec +82 -0
  167. data/parse-stack.png +0 -0
  168. data/scripts/debug-ips.js +35 -0
  169. data/scripts/docker/Dockerfile.parse +13 -0
  170. data/scripts/docker/atlas-init.js +284 -0
  171. data/scripts/docker/docker-compose.atlas.yml +76 -0
  172. data/scripts/docker/docker-compose.test.yml +106 -0
  173. data/scripts/docker/mongo-init.js +21 -0
  174. data/scripts/eval_mcp_with_lm_studio.rb +274 -0
  175. data/scripts/start-parse.sh +90 -0
  176. data/scripts/start_mcp_server.rb +78 -0
  177. data/scripts/test_server_connection.rb +82 -0
  178. 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