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,443 @@
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_model/serializers/json"
9
+ require_relative "model"
10
+
11
+ module Parse
12
+
13
+ # The Pointer class represents the pointer type in Parse and is the superclass
14
+ # of Parse::Object types. A pointer can be considered a type of Parse::Object
15
+ # in which only the class name and id is known. In most cases, you may not
16
+ # deal with Parse::Pointer objects directly if you have defined all your
17
+ # Parse::Object subclasses.
18
+ #
19
+ # A `Parse::Pointer` only contains data about the specific Parse class and
20
+ # the `id` for the object. Therefore, creating an instance of any
21
+ # Parse::Object subclass with only the `:id` field set will be
22
+ # considered in "pointer" state even though its specific class is not
23
+ # `Parse::Pointer` type. The only case that you may have a Parse::Pointer
24
+ # is in the case where an object was received for one of your classes and
25
+ # the framework has no registered class handler for it.
26
+ # Assume you have the tables `Post`, `Comment` and `Author` defined in your
27
+ # remote Parse database, but have only defined `Post` and `Commentary`
28
+ # locally.
29
+ # @example
30
+ # class Post < Parse::Object
31
+ # end
32
+ #
33
+ # class Commentary < Parse::Object
34
+ # belongs_to :post
35
+ # belongs_to :author
36
+ # end
37
+ #
38
+ # comment = Commentary.first
39
+ # comment.post? # true because it is non-nil
40
+ # comment.artist? # true because it is non-nil
41
+ #
42
+ # # both are true because they are in a Pointer state
43
+ # comment.post.pointer? # true
44
+ # comment.author.pointer? # true
45
+ #
46
+ # # we have defined a Post class handler
47
+ # comment.post # <Post @parse_class="Post", @id="xdqcCqfngz">
48
+ #
49
+ # # we have not defined an Author class handler
50
+ # comment.author # <Parse::Pointer @parse_class="Author", @id="hZLbW6ofKC">
51
+ #
52
+ #
53
+ # comment.post.fetch # fetch the relation
54
+ # comment.post.pointer? # false, it is now a full object.
55
+ #
56
+ # The effect is that for any unknown classes that the framework encounters,
57
+ # it will generate Parse::Pointer instances until you define those classes
58
+ # with valid properties and associations. While this might be ok for some
59
+ # classes you do not use, we still recommend defining all your Parse classes
60
+ # locally in the framework.
61
+ #
62
+ # Once you have a subclass, you may also create a Parse::Pointer object using
63
+ # the _pointer_ method.
64
+ # @example
65
+ # Parse::User.pointer("123456") # => Parse::Pointer for "_User" class
66
+ #
67
+ # @see Parse::Object
68
+ class Pointer < Model
69
+ # The default attributes in a Parse Pointer hash.
70
+ ATTRIBUTES = { __type: :string, className: :string, objectId: :string }.freeze
71
+ # Permitted character set + length for a Parse objectId. Parse Server
72
+ # itself generates 10-char `[A-Za-z0-9]` ids; with
73
+ # `allowCustomObjectId: true` apps can pass arbitrary identifiers, so
74
+ # we accept the wider URL-safe set `[A-Za-z0-9_.-]` and cap length
75
+ # at 64. Anything OUTSIDE this set (`/`, `\`, CR/LF, `?`, `&`, `#`,
76
+ # `%`, quotes, angle brackets, semicolons, whitespace) is rejected
77
+ # — those are the bytes that turn a `Pointer.id=` write into a
78
+ # path-traversal, header-injection, or batch-op-path-poisoning
79
+ # vector when interpolated into REST URLs or batch op `path` fields.
80
+ OBJECT_ID_FORMAT = /\A[A-Za-z0-9_.\-]{1,64}\z/.freeze
81
+
82
+ # @return [String] the name of the Parse class for this pointer.
83
+ attr_accessor :parse_class
84
+ # @return [String] the objectId field
85
+ attr_reader :id
86
+
87
+ # Assign the Parse objectId. Empty / nil values are permitted (Pointer
88
+ # in unbound state); non-empty values must match {OBJECT_ID_FORMAT}.
89
+ # @raise [ArgumentError] when +value+ is a non-empty string that does
90
+ # not match the format.
91
+ def id=(value)
92
+ if value.nil?
93
+ @id = nil
94
+ return
95
+ end
96
+ str = value.to_s
97
+ if str.empty?
98
+ @id = str
99
+ return
100
+ end
101
+ unless OBJECT_ID_FORMAT.match?(str)
102
+ raise ArgumentError,
103
+ "Invalid Parse objectId #{str.inspect}: must match #{OBJECT_ID_FORMAT.source}. " \
104
+ "Refusing to assign a value that could be a path-traversal or injection payload."
105
+ end
106
+ @id = str
107
+ end
108
+
109
+ # @return [Model::TYPE_POINTER]
110
+ def __type; Parse::Model::TYPE_POINTER; end
111
+
112
+ alias_method :className, :parse_class
113
+ # A Parse object as a className field and objectId. In ruby, we will use the
114
+ # id attribute method, but for usability, we will also alias it to objectId
115
+ alias_method :objectId, :id
116
+
117
+ # A Parse pointer only requires the name of the remote Parse collection name,
118
+ # and the `objectId` of the record.
119
+ # @param table [String] The Parse class name in the Parse database.
120
+ # @param oid [String] The objectId
121
+ def initialize(table, oid)
122
+ @parse_class = table.to_s
123
+ self.id = oid
124
+ end
125
+
126
+ # @return [String] the name of the collection for this Pointer.
127
+ def parse_class
128
+ @parse_class
129
+ end
130
+
131
+ # @return [String] a string representing the class and id of this instance.
132
+ def sig
133
+ "#{@parse_class}##{id || "new"}"
134
+ end
135
+
136
+ # @return [Hash]
137
+ def attributes
138
+ ATTRIBUTES
139
+ end
140
+
141
+ # @return [Hash] serialized JSON structure
142
+ def json_hash
143
+ JSON.parse to_json
144
+ end
145
+
146
+ # Create a new pointer with the current class name and id. While this may not make sense
147
+ # for a pointer instance, Parse::Object subclasses use this inherited method to turn themselves into
148
+ # pointer objects.
149
+ # @example
150
+ # user = Parse::User.first
151
+ # user.pointer # => Parse::Pointer("_User", user.id)
152
+ #
153
+ # @return [Pointer] a new Pointer for this object.
154
+ # @see Parse::Object
155
+ def pointer
156
+ Pointer.new parse_class, @id
157
+ end
158
+
159
+ # Whether this instance is in pointer state. A pointer is determined
160
+ # if we have a parse class and an id, but no created_at or updated_at fields.
161
+ # @return [Boolean] true if instance is in pointer state.
162
+ def pointer?
163
+ present? && @created_at.blank? && @updated_at.blank?
164
+ end
165
+
166
+ # Returns true if the data for this instance has been fetched. Because of some autofetching
167
+ # mechanisms, this is useful to know whether the object already has data without actually causing
168
+ # a fetch of the data.
169
+ # @return [Boolean] true if not in pointer state.
170
+ def fetched?
171
+ present? && pointer? == false
172
+ end
173
+
174
+ # This method is a general implementation that gets overriden by Parse::Object subclass.
175
+ # Given the class name and the id, we will go to Parse and fetch the actual record, returning the
176
+ # Parse::Object by default.
177
+ # @overload fetch
178
+ # Full fetch - fetches all fields
179
+ # @return [Parse::Object] the fetched Parse::Object, nil otherwise.
180
+ # @overload fetch(return_object)
181
+ # Legacy signature for backward compatibility.
182
+ # @param return_object [Boolean] if true returns object, if false returns JSON
183
+ # @return [Parse::Object, Hash] the object or raw JSON data
184
+ # @overload fetch(keys:, includes:, cache:)
185
+ # Partial fetch - fetches only specified fields
186
+ # @param keys [Array<Symbol, String>, nil] optional list of fields to fetch (partial fetch).
187
+ # @param includes [Array<String>, nil] optional list of pointer fields to expand.
188
+ # @param cache [Boolean, Symbol, Integer] caching mode:
189
+ # - true - read from and write to cache
190
+ # - false - completely bypass cache
191
+ # - :write_only - skip cache read, but update cache with fresh data
192
+ # - Integer - cache for specific number of seconds
193
+ # @return [Parse::Object] a partially fetched Parse::Object, nil otherwise.
194
+ def fetch(return_object = nil, keys: nil, includes: nil, cache: nil)
195
+ # Handle legacy signature: fetch(false) returns JSON
196
+ if return_object == false
197
+ return fetch_json(keys: keys, includes: includes)
198
+ end
199
+
200
+ # Build query parameters for partial fetch
201
+ query = {}
202
+ if keys.present?
203
+ keys_array = Array(keys).map { |k| Parse::Query.format_field(k) }
204
+ query[:keys] = keys_array.join(",")
205
+ end
206
+ if includes.present?
207
+ includes_array = Array(includes).map(&:to_s)
208
+ query[:include] = includes_array.join(",")
209
+ end
210
+
211
+ # Build opts for caching
212
+ opts = {}
213
+ opts[:cache] = cache unless cache.nil?
214
+
215
+ response = client.fetch_object(parse_class, id, query: query.presence, **opts)
216
+ return nil if response.error?
217
+
218
+ # Check if the result is empty - this indicates object not found
219
+ result = response.result
220
+ if result.nil? || (result.is_a?(Array) && result.empty?)
221
+ return nil
222
+ end
223
+
224
+ # Convert the JSON result to a proper Parse::Object
225
+ return nil unless result.is_a?(Hash)
226
+
227
+ # Try to find the appropriate Parse class, fallback to Parse::Object
228
+ klass = Parse::Model.find_class(parse_class) || Parse::Object
229
+
230
+ # For partial fetch, build with fetched_keys tracking
231
+ if keys.present?
232
+ # Parse keys to get top-level field names and nested keys
233
+ top_level_keys = Array(keys).map { |k| Parse::Query.format_field(k).split(".").first.to_sym }
234
+ top_level_keys << :id unless top_level_keys.include?(:id)
235
+ top_level_keys << :objectId unless top_level_keys.include?(:objectId)
236
+ top_level_keys.uniq!
237
+
238
+ # Parse dot notation into nested fetched keys
239
+ nested_keys = Parse::Query.parse_keys_to_nested_keys(Array(keys))
240
+
241
+ obj = klass.build(result, parse_class, fetched_keys: top_level_keys, nested_fetched_keys: nested_keys.presence)
242
+ else
243
+ # Full fetch - create without partial fetch tracking. Trusted
244
+ # hydration: +result+ is the server response body, which
245
+ # legitimately carries +createdAt+/+updatedAt+/+sessionToken+
246
+ # and other PROTECTED_MASS_ASSIGNMENT_KEYS. The +@_trusted_init+
247
+ # ivar tells {Parse::Object#initialize} to skip the protected-key
248
+ # filter — see that method for why we don't use a kwarg.
249
+ obj = klass.allocate
250
+ obj.instance_variable_set(:@_trusted_init, true)
251
+ obj.send(:initialize, result)
252
+ end
253
+
254
+ obj.clear_changes! if obj.respond_to?(:clear_changes!)
255
+ obj
256
+ end
257
+
258
+ # Returns raw JSON data from the server without creating an object.
259
+ # @param keys [Array<Symbol, String>, nil] optional list of fields to fetch.
260
+ # @param includes [Array<String>, nil] optional list of pointer fields to expand.
261
+ # @return [Hash, nil] the raw JSON data or nil if error.
262
+ def fetch_json(keys: nil, includes: nil)
263
+ query = {}
264
+ if keys.present?
265
+ keys_array = Array(keys).map { |k| Parse::Query.format_field(k) }
266
+ query[:keys] = keys_array.join(",")
267
+ end
268
+ if includes.present?
269
+ includes_array = Array(includes).map(&:to_s)
270
+ query[:include] = includes_array.join(",")
271
+ end
272
+
273
+ response = client.fetch_object(parse_class, id, query: query.presence)
274
+ return nil if response.error?
275
+ response.result
276
+ end
277
+
278
+ # Fetches the Parse object from the data store and returns a Parse::Object instance.
279
+ # This is a convenience method that calls fetch.
280
+ # @return [Parse::Object] the fetched Parse::Object, nil otherwise.
281
+ def fetch_object
282
+ fetch
283
+ end
284
+
285
+ # Fetches the pointer with explicit caching enabled and returns a Parse::Object.
286
+ # This is a convenience method that calls fetch with cache: true.
287
+ # Use this when you want to leverage cached responses for better performance.
288
+ # @param keys [Array<Symbol, String>, nil] optional list of fields to fetch (partial fetch).
289
+ # @param includes [Array<String>, nil] optional list of pointer fields to expand.
290
+ # @return [Parse::Object] the fetched Parse::Object, nil otherwise.
291
+ # @example Fetch pointer with caching
292
+ # capture = capture_pointer.fetch_cache!
293
+ # @example Partial fetch with caching
294
+ # capture = capture_pointer.fetch_cache!(keys: [:title, :status])
295
+ # @see #fetch
296
+ def fetch_cache!(keys: nil, includes: nil)
297
+ fetch(keys: keys, includes: includes, cache: true)
298
+ end
299
+
300
+ # Two Parse::Pointers (or Parse::Objects) are equal if both of them have
301
+ # the same Parse class and the same id.
302
+ # @return [Boolean]
303
+ def ==(o)
304
+ return false unless o.is_a?(Pointer)
305
+ #only equal if the Parse class and object ID are the same.
306
+ self.parse_class == o.parse_class && id == o.id
307
+ end
308
+
309
+ alias_method :eql?, :==
310
+
311
+ # Compute a hash-code for this object based on identity (class and id).
312
+ # This is consistent with the == method which compares by parse_class and id.
313
+ #
314
+ # Two objects with the same class and id will have the same hash code
315
+ # regardless of their dirty state or other attributes. This is important for:
316
+ # - Array operations (uniq, &, |) to work correctly based on identity
317
+ # - Hash key lookups to find objects by identity
318
+ # - Set operations
319
+ #
320
+ # @return [Integer] hash code based on class name and object id
321
+ def hash
322
+ [parse_class, id].hash
323
+ end
324
+
325
+ # @return [Boolean] true if instance has a Parse class and an id.
326
+ def present?
327
+ parse_class.present? && @id.present?
328
+ end
329
+
330
+ # Access the pointer properties through hash accessor. This is done for
331
+ # compatibility with the hash access of a Parse::Object. This method
332
+ # returns nil if the key is not one of: :id, :objectId, or :className.
333
+ # @param key [String] the name of the property.
334
+ # @return [Object] the value for this key.
335
+ def [](key)
336
+ return nil unless [:id, :objectId, :className].include?(key.to_sym)
337
+ send(key)
338
+ end
339
+
340
+ # Handles method calls for properties that exist on the target model class.
341
+ # When a property is accessed on a Pointer, this will auto-fetch the object
342
+ # and delegate the method call to the fetched object.
343
+ #
344
+ # If Parse.autofetch_raise_on_missing_keys is enabled, this will raise
345
+ # Parse::AutofetchTriggeredError instead of fetching.
346
+ #
347
+ # @example
348
+ # pointer = Post.pointer("abc123")
349
+ # pointer.title # auto-fetches and returns title
350
+ #
351
+ # @param method_name [Symbol] the method being called
352
+ # @param args [Array] arguments to the method
353
+ # @param block [Proc] optional block
354
+ # @return [Object] the result of calling the method on the fetched object
355
+ # @raise [Parse::AutofetchTriggeredError] if autofetch_raise_on_missing_keys is enabled
356
+ def method_missing(method_name, *args, &block)
357
+ # Try to find the model class for this pointer
358
+ klass = Parse::Model.find_class(parse_class)
359
+
360
+ # If no class is registered or the class doesn't have this field, use default behavior
361
+ unless klass && klass.respond_to?(:fields) && klass.fields[method_name.to_s.chomp("=").to_sym]
362
+ return super
363
+ end
364
+
365
+ # We have a registered class with this field - handle autofetch
366
+ field_name = method_name.to_s.chomp("=").to_sym
367
+
368
+ # If autofetch_raise_on_missing_keys is enabled, raise an error
369
+ if Parse.autofetch_raise_on_missing_keys
370
+ raise Parse::AutofetchTriggeredError.new(klass, id, field_name, is_pointer: true)
371
+ end
372
+
373
+ # Log info about autofetch being triggered
374
+ if Parse.warn_on_query_issues
375
+ puts "[Parse::Autofetch] Fetching #{parse_class}##{id} - pointer accessed field :#{field_name} (silence with Parse.warn_on_query_issues = false)"
376
+ end
377
+
378
+ # Fetch the object and delegate the method call
379
+ @_fetched_object ||= fetch
380
+ return nil unless @_fetched_object
381
+
382
+ @_fetched_object.send(method_name, *args, &block)
383
+ end
384
+
385
+ # Indicates whether this object responds to methods that would trigger autofetch.
386
+ # Returns true for properties defined on the target model class.
387
+ #
388
+ # @param method_name [Symbol] the method name to check
389
+ # @param include_private [Boolean] whether to include private methods
390
+ # @return [Boolean] true if the method can be handled
391
+ def respond_to_missing?(method_name, include_private = false)
392
+ klass = Parse::Model.find_class(parse_class)
393
+ if klass && klass.respond_to?(:fields)
394
+ field_name = method_name.to_s.chomp("=").to_sym
395
+ return true if klass.fields[field_name]
396
+ end
397
+ super
398
+ end
399
+
400
+ # Set the pointer properties through hash accessor. This is done for
401
+ # compatibility with the hash access of a Parse::Object. This method
402
+ # does nothing if the key is not one of: :id, :objectId, or :className.
403
+ # @param key (see #[])
404
+ # @return [Object]
405
+ def []=(key, value)
406
+ return unless [:id, :objectId, :className].include?(key.to_sym)
407
+ send("#{key}=", value)
408
+ end
409
+ end
410
+ end
411
+
412
+ # extensions
413
+ class Array
414
+ # This method maps all the ids (String) of all Parse::Objects in the array.
415
+ # @return [Array<String>] an array of strings of ids.
416
+ def objectIds
417
+ map { |m| m.is_a?(Parse::Pointer) ? m.id : nil }.compact
418
+ end
419
+
420
+ # Filter all objects in the array that do not inherit from Parse::Pointer or
421
+ # Parse::Object.
422
+ # @return [Array<Parse::Object,Parse::Pointer>] an array of Parse::Objects.
423
+ def valid_parse_objects
424
+ select { |s| s.is_a?(Parse::Pointer) }
425
+ end
426
+
427
+ # Convert all potential objects in the array to a list of Parse::Pointer instances.
428
+ # The array can contain a mixture of objects types including JSON Parse-like hashes.
429
+ # @return [Array<Parse::Pointer>] an array of Parse::Pointer objects.
430
+ def parse_pointers(table = nil)
431
+ self.map do |m|
432
+ #if its an exact Parse::Pointer
433
+ if m.is_a?(Parse::Pointer) || m.respond_to?(:pointer)
434
+ next m.pointer
435
+ elsif m.is_a?(Hash) && m[Parse::Model::KEY_CLASS_NAME] && m[Parse::Model::OBJECT_ID]
436
+ next Parse::Pointer.new m[Parse::Model::KEY_CLASS_NAME], m[Parse::Model::OBJECT_ID]
437
+ elsif m.is_a?(Hash) && m[:className] && m[:objectId]
438
+ next Parse::Pointer.new m[:className], m[:objectId]
439
+ end
440
+ nil
441
+ end.compact
442
+ end
443
+ end