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,1104 @@
1
+ require "faraday"
2
+
3
+ # Attempt to load the persistent connection adapter for better performance.
4
+ # Falls back gracefully to the default adapter if not available.
5
+ NET_HTTP_PERSISTENT_AVAILABLE = begin
6
+ require "faraday/net_http_persistent"
7
+ true
8
+ rescue LoadError
9
+ warn "[parse-stack] faraday-net_http_persistent gem not available. " \
10
+ "Using standard Net::HTTP adapter. For better performance, add " \
11
+ "'faraday-net_http_persistent' to your Gemfile."
12
+ false
13
+ end
14
+
15
+ require "active_support"
16
+ require "moneta"
17
+ require "active_model/serialization"
18
+ require "active_model/serializers/json"
19
+ require "active_support/inflector"
20
+ require "active_support/core_ext/object"
21
+ require "active_support/core_ext/string"
22
+ require "active_support/core_ext/date/calculations"
23
+ require "active_support/core_ext/date_time/calculations"
24
+ require "active_support/core_ext/time/calculations"
25
+ require "active_support/core_ext"
26
+ require_relative "client/request"
27
+ require_relative "client/response"
28
+ require_relative "client/batch"
29
+ require_relative "client/body_builder"
30
+ require_relative "client/authentication"
31
+ require_relative "client/caching"
32
+ require_relative "client/logging"
33
+ require_relative "client/profiling"
34
+ require_relative "api/all"
35
+
36
+ module Parse
37
+ class Error < StandardError
38
+ # An error when a general connection occurs.
39
+ class ConnectionError < Error; end
40
+
41
+ # An error when a connection timeout occurs.
42
+ class TimeoutError < Error; end
43
+
44
+ # An error when there is an Parse REST API protocol error.
45
+ class ProtocolError < Error; end
46
+
47
+ # An error when the Parse server returned invalid code.
48
+ class ServerError < Error; end
49
+
50
+ # An error when a Parse server responds with HTTP 500.
51
+ class ServiceUnavailableError < Error; end
52
+
53
+ # An error when the authentication credentials in the request are invalid.
54
+ class AuthenticationError < Error; end
55
+
56
+ # An error when the burst limit has been exceeded.
57
+ class RequestLimitExceededError < Error; end
58
+
59
+ # An error when the session token provided in the request is invalid.
60
+ class InvalidSessionTokenError < Error; end
61
+
62
+ # An error raised when a cloud function or job returns an error response
63
+ # (e.g. when the cloud code calls error!()). Carries the function name,
64
+ # Parse error code, HTTP status, and the underlying Response for debugging.
65
+ class CloudCodeError < Error
66
+ attr_reader :function_name, :code, :http_status, :response
67
+
68
+ def initialize(function_name, response)
69
+ @function_name = function_name
70
+ @response = response
71
+ @code = response.code
72
+ @http_status = response.http_status
73
+ super("Parse cloud function `#{function_name}` failed: [#{@code}] #{response.error} (HTTP #{@http_status})")
74
+ end
75
+
76
+ def inspect
77
+ "#<#{self.class} function=#{@function_name.inspect} code=#{@code.inspect} http_status=#{@http_status.inspect}>"
78
+ end
79
+ end
80
+ end
81
+
82
+ # Retrieve the App specific Parse configuration parameters. The configuration
83
+ # for a connection is cached after the first request. Use the bang version to
84
+ # force update from the Parse backend.
85
+ # @example
86
+ # val = Parse.config["myKey"]
87
+ # val = Parse.config["myKey"] # cached
88
+ # @see Parse.config!
89
+ # @param conn [Symbol] the name of the client connection to use.
90
+ # @return [Hash] the Parse config hash for the session.
91
+ def self.config(conn = :default)
92
+ Parse::Client.client(conn).config
93
+ end
94
+
95
+ # Set a parameter in the Parse configuration for an application.
96
+ # @example
97
+ # # update a config with Parse
98
+ # Parse.set_config "myKey", "someValue"
99
+ # # mark a single key as master-key-only
100
+ # Parse.set_config "myKey", "someValue", master_key_only: true
101
+ # @param field [String] the name configuration variable.
102
+ # @param value [Object] the value configuration variable. Only Parse types are supported.
103
+ # @param conn [Symbol] the name of the client connection to use.
104
+ # @param master_key_only [Boolean, nil] when not nil, sets the masterKeyOnly
105
+ # flag for `field` to the given boolean value in the same request.
106
+ # @return [Hash] the Parse config hash for the session.
107
+ def self.set_config(field, value, conn = :default, master_key_only: nil)
108
+ opts = master_key_only.nil? ? {} : { master_key_only: { field.to_s => !!master_key_only } }
109
+ Parse::Client.client(conn).update_config({ field => value }, **opts)
110
+ end
111
+
112
+ # Set a key value pairs in the Parse configuration for an application.
113
+ # @example
114
+ # # batch update several
115
+ # Parse.update_config({fieldEnabled: true, searchMiles: 50})
116
+ # # also mark some keys as master-key-only
117
+ # Parse.update_config({fieldEnabled: true}, master_key_only: { fieldEnabled: true })
118
+ # @param params [Hash] a set of key value pairs to set in the Parse configuration.
119
+ # @param conn [Symbol] the name of the client connection to use.
120
+ # @param master_key_only [Hash{String=>Boolean}, nil] optional map of config
121
+ # keys to boolean masterKeyOnly flags. Parse Server merges this with any
122
+ # existing masterKeyOnly settings; unspecified keys keep their current flag.
123
+ # @return [Hash] the Parse config hash for the session.
124
+ def self.update_config(params, conn = :default, master_key_only: nil)
125
+ opts = master_key_only.nil? ? {} : { master_key_only: master_key_only }
126
+ Parse::Client.client(conn).update_config(params, **opts)
127
+ end
128
+
129
+ # Force fetch updated Parse configuration
130
+ # @param conn [Symbol] the name of the client connection to use.
131
+ # @return [Hash] the Parse configuration
132
+ def self.config!(conn = :default)
133
+ Parse::Client.client(conn).config!
134
+ end
135
+
136
+ # Return every config entry zipped with its masterKeyOnly trait.
137
+ # @example
138
+ # Parse.config_entries
139
+ # # => { "fieldA" => { value: "x", master_key_only: false } }
140
+ # Parse.config_entries(master: true)
141
+ # # => { "fieldA" => { value: "x", master_key_only: false },
142
+ # # "fieldB" => { value: 42, master_key_only: true } }
143
+ # @param conn [Symbol] the name of the client connection to use.
144
+ # @param master [Boolean] when true, include master-key-only entries.
145
+ # @return [Hash{String=>Hash}] map of config key to `{value:, master_key_only:}`.
146
+ def self.config_entries(conn = :default, master: false)
147
+ Parse::Client.client(conn).config_entries(master: master)
148
+ end
149
+
150
+ # Retrieve the masterKeyOnly flag map for the application configuration.
151
+ # @example
152
+ # Parse.master_key_only["secretKey"] # => true
153
+ # @param conn [Symbol] the name of the client connection to use.
154
+ # @return [Hash{String=>Boolean}] map of config keys to masterKeyOnly flags,
155
+ # or an empty hash if the server did not return one.
156
+ def self.master_key_only(conn = :default)
157
+ Parse::Client.client(conn).master_key_only
158
+ end
159
+
160
+ # Helper method to get the default Parse client.
161
+ # @param conn [Symbol] the name of the client connection to use. Defaults to :default
162
+ # @return [Parse::Client] a client object for the connection name.
163
+ def self.client(conn = :default)
164
+ Parse::Client.client(conn)
165
+ end
166
+
167
+ # The shared cache for the default client connection. This is useful if you want to
168
+ # also utilize the same cache store for other purposes in your application.
169
+ # This should normally be a {https://github.com/minad/moneta Moneta} unified
170
+ # cache interface.
171
+ # @return [Moneta::Transformer,Moneta::Expires] the cache instance
172
+ # @see Parse::Client#cache
173
+ def self.cache
174
+ @shared_cache ||= Parse::Client.client(:default).cache
175
+ end
176
+
177
+ # This class is the core and low level API for the Parse SDK REST interface that
178
+ # is used by the other components. It can manage multiple sessions, which means
179
+ # you can have multiple client instances pointing to different Parse Applications
180
+ # at the same time. It handles sending raw requests as well as providing
181
+ # Request/Response objects for all API handlers. The connection engine is
182
+ # Faraday, which means it is open to add any additional middleware for
183
+ # features you'd like to implement.
184
+ class Client
185
+ include Parse::API::Analytics
186
+ include Parse::API::Aggregate
187
+ include Parse::API::Batch
188
+ include Parse::API::CloudFunctions
189
+ include Parse::API::Config
190
+ include Parse::API::Files
191
+ include Parse::API::Hooks
192
+ include Parse::API::Objects
193
+ include Parse::API::Push
194
+ include Parse::API::Schema
195
+ include Parse::API::Server
196
+ include Parse::API::Sessions
197
+ include Parse::API::Users
198
+ # The user agent header key.
199
+ USER_AGENT_HEADER = "User-Agent".freeze
200
+ # The value for the User-Agent header.
201
+ USER_AGENT_VERSION = "Parse-Stack v#{Parse::Stack::VERSION}".freeze
202
+ # The default retry count
203
+ DEFAULT_RETRIES = 2
204
+ # The wait time in seconds between retries
205
+ RETRY_DELAY = 1.5
206
+
207
+ # An error when a general response error occurs when communicating with Parse server.
208
+ class ResponseError < Parse::Error; end
209
+
210
+ # An error when a Parse server response carries code 137 (DuplicateValue),
211
+ # typically raised when a unique field (or MongoDB unique index) rejects an
212
+ # insert. Carries the {Parse::Response} for inspection. The synchronize-create
213
+ # wrapper in {Parse::Core::Actions} rescues this internally and re-queries
214
+ # inside the held lock to return the winning object.
215
+ #
216
+ # **Message redaction.** Parse Server (and the underlying MongoDB driver)
217
+ # serialize the offending unique-key payload into the error string in two
218
+ # parallel forms: `keyValue: { "email": "user@example.com" }` AND
219
+ # `dup key: { : "user@example.com" }`. Echoing either into application
220
+ # logs exposes the colliding identifier (email, username, account number,
221
+ # external ID) to anyone with log access — turning a duplicate-write
222
+ # error into a unique-field enumeration oracle. The constructor strips
223
+ # both fragments before delegating to `super`. The raw response is
224
+ # preserved on `#response` for callers that legitimately need the
225
+ # unredacted detail (e.g. the synchronize-create wrapper).
226
+ class DuplicateValueError < ResponseError
227
+ CODE = 137
228
+ # Matches both MongoDB E11000 fragment forms: `keyValue: { ... }`
229
+ # and `dup key: { ... }`. The driver emits the offending unique-key
230
+ # value verbatim in each, so both must be stripped to close the leak.
231
+ KEY_VALUE_PATTERN = /(?:keyValue|dup\s*key)\s*:?\s*\{[^}]*\}/i.freeze
232
+ REDACTION = "[REDACTED]".freeze
233
+
234
+ attr_reader :response
235
+
236
+ def initialize(response = nil)
237
+ @response = response
238
+ raw = if response.is_a?(String)
239
+ response
240
+ elsif response.respond_to?(:error)
241
+ response.error
242
+ else
243
+ response.to_s
244
+ end
245
+ super(self.class.redact(raw))
246
+ end
247
+
248
+ # Strip `keyValue: { ... }` fragments from a message string so the
249
+ # offending unique-constraint value never leaks into log lines.
250
+ # Returns the original message verbatim when it contains no
251
+ # `keyValue:` token, so non-MongoDB-shaped errors are unaffected.
252
+ # @param msg [String, nil]
253
+ # @return [String, nil]
254
+ def self.redact(msg)
255
+ return msg if msg.nil?
256
+ s = msg.to_s
257
+ s.gsub(KEY_VALUE_PATTERN, REDACTION)
258
+ end
259
+ end
260
+
261
+ # @!attribute cache
262
+ # The underlying cache store for caching API requests.
263
+ # @see Parse.cache
264
+ # @return [Moneta::Transformer,Moneta::Expires]
265
+ # @!attribute [r] application_id
266
+ # The Parse application identifier to be sent in every API request.
267
+ # @return [String]
268
+ # @!attribute [r] api_key
269
+ # The Parse API key to be sent in every API request.
270
+ # @return [String]
271
+ # @!attribute [r] master_key
272
+ # The Parse master key for this application, which when set, will be sent
273
+ # in every API request. (There is a way to prevent this on a per request basis.)
274
+ # @return [String]
275
+ # @!attribute [r] server_url
276
+ # The Parse server url that will be receiving these API requests. By default
277
+ # this will be {Parse::Protocol::SERVER_URL}.
278
+ # @return [String]
279
+ # @!attribute retry_limit
280
+ # The default retry count for the client when a specific request timesout or
281
+ # the service is unavailable. Defaults to {DEFAULT_RETRIES}.
282
+ # @return [String]
283
+ attr_accessor :cache
284
+ attr_writer :retry_limit
285
+ attr_reader :application_id, :api_key, :master_key, :server_url
286
+ alias_method :app_id, :application_id
287
+ # The client can support multiple sessions. The first session created, will be placed
288
+ # under the default session tag. The :default session will be the default client to be used
289
+ # by the other classes including Parse::Query and Parse::Objects
290
+ @clients = { default: nil }
291
+ class << self
292
+ # @!attribute [r] clients
293
+ # A hash of Parse::Client instances.
294
+ # @return [Hash<Parse::Client>]
295
+ attr_reader :clients
296
+
297
+ # @param conn [Symbol] the name of the connection.
298
+ # @return [Boolean] true if a Parse::Client has been configured.
299
+ def client?(conn = :default)
300
+ @clients[conn].present?
301
+ end
302
+
303
+ # Returns or create a new Parse::Client connection for the given connection
304
+ # name.
305
+ # @param conn [Symbol] the name of the connection.
306
+ # @return [Parse::Client]
307
+ def client(conn = :default)
308
+ @clients[conn] ||= self.new
309
+ end
310
+
311
+ # Setup the a new client with the appropriate Parse app keys, middleware and
312
+ # options.
313
+ # @example
314
+ # Parse.setup app_id: "YOUR_APP_ID",
315
+ # api_key: "YOUR_REST_API_KEY",
316
+ # master_key: "YOUR_MASTER_KEY", # optional
317
+ # server_url: 'https://localhost:1337/parse' #default
318
+ # @param opts (see Parse::Client#initialize)
319
+ # @option opts (see Parse::Client#initialize)
320
+ # @yield the block for additional configuration with Faraday middleware.
321
+ # @return (see Parse::Client#initialize)
322
+ # @see Parse::Middleware::BodyBuilder
323
+ # @see Parse::Middleware::Caching
324
+ # @see Parse::Middleware::Authentication
325
+ # @see Parse::Protocol
326
+ def setup(opts = {}, &block)
327
+ @clients[:default] = self.new(opts, &block)
328
+ end
329
+
330
+ # @!visibility private
331
+ # Emit a redacted warning about a Parse::Response error to stderr.
332
+ #
333
+ # Routes the response error string through
334
+ # {Parse::Middleware::BodyBuilder.redact} to strip credentials (passwords,
335
+ # tokens, sessionTokens, access_tokens, authData) before logging, and
336
+ # truncates to {SAFE_WARN_MAX_ERROR_LENGTH} chars.
337
+ #
338
+ # @param tag [String] the bracketed prefix (e.g. "AuthenticationError").
339
+ # @param response [Parse::Response] the response carrying the error.
340
+ # @param name [String, nil] optional cloud-function or job name for context.
341
+ # @return [nil]
342
+ def _safe_warn(tag, response, name: nil)
343
+ err = Parse::Middleware::BodyBuilder.redact(response.error.to_s)[0, SAFE_WARN_MAX_ERROR_LENGTH]
344
+ if name
345
+ warn "[Parse:#{tag}] `#{name}` [#{response.code}] #{err} (HTTP #{response.http_status})"
346
+ else
347
+ warn "[Parse:#{tag}] [E-#{response.code}] #{response.request} : #{err} (#{response.http_status})"
348
+ end
349
+ nil
350
+ end
351
+ end
352
+
353
+ # @!visibility private
354
+ # Maximum number of characters of a Parse::Response error string to include
355
+ # in safe warn output. Bounds log volume from chatty server errors or
356
+ # misbehaving cloud functions.
357
+ SAFE_WARN_MAX_ERROR_LENGTH = 200
358
+
359
+ # Create a new client connected to the Parse Server REST API endpoint.
360
+ # @param opts [Hash] a set of connection options to configure the client.
361
+ # @option opts [String] :server_url The server url of your Parse Server if you
362
+ # are not using the hosted Parse service. By default it will use
363
+ # ENV["PARSE_SERVER_URL"] if available, otherwise fallback to {Parse::Protocol::SERVER_URL}.
364
+ # @option opts [String] :app_id The Parse application id. Defaults to
365
+ # ENV['PARSE_SERVER_APPLICATION_ID'].
366
+ # @option opts [String] :api_key Your Parse REST API Key. Defaults to ENV['PARSE_SERVER_REST_API_KEY'].
367
+ # @option opts [String] :master_key The Parse application master key (optional).
368
+ # If this key is set, it will be sent on every request sent by the client
369
+ # and your models. Defaults to ENV['PARSE_SERVER_MASTER_KEY'].
370
+ # @option opts [Boolean, Symbol] :logging Controls request/response logging.
371
+ # - `true` - Enable logging at :info level
372
+ # - `:debug` - Enable verbose logging with headers and body content
373
+ # - `:warn` - Only log errors and warnings
374
+ # - `false` or `nil` - Disable logging (default)
375
+ # This configures both the new {Parse::Middleware::Logging} middleware
376
+ # and the legacy {Parse::Middleware::BodyBuilder} logging.
377
+ # @option opts [Logger] :logger A custom logger instance for request/response logging.
378
+ # Defaults to Logger.new(STDOUT) if not specified.
379
+ # @option opts [Object] :adapter The connection adapter. By default it uses
380
+ # `:net_http_persistent` for connection pooling. Set `connection_pooling: false`
381
+ # to use the standard `Faraday.default_adapter` (Net/HTTP) instead.
382
+ # @option opts [Boolean, Hash] :connection_pooling Controls HTTP connection pooling.
383
+ # Defaults to `true`, using the `:net_http_persistent` adapter for improved
384
+ # performance through connection reuse. Set to `false` to disable pooling
385
+ # and create a new connection for each request. This option is ignored if
386
+ # `:adapter` is explicitly specified.
387
+ # Pass a Hash to enable pooling with custom configuration:
388
+ # - `:pool_size` [Integer] - Number of connections per thread (default: 1)
389
+ # - `:idle_timeout` [Integer] - Seconds before closing idle connections (default: 5)
390
+ # - `:keep_alive` [Integer] - HTTP Keep-Alive timeout in seconds
391
+ # @example Custom connection pooling
392
+ # Parse.setup(
393
+ # connection_pooling: { pool_size: 5, idle_timeout: 60, keep_alive: 60 }
394
+ # )
395
+ # @option opts [Moneta::Transformer,Moneta::Expires] :cache A caching adapter of type
396
+ # {https://github.com/minad/moneta Moneta::Transformer} or
397
+ # {https://github.com/minad/moneta Moneta::Expires} that will be used
398
+ # by the caching middleware {Parse::Middleware::Caching}.
399
+ # Caching queries and object fetches can help improve the performance of
400
+ # your application, even if it is for a few seconds. Only successful GET
401
+ # object fetches and non-empty result queries will be cached by default.
402
+ # You may set the default expiration time with the expires option.
403
+ # At any point in time you may clear the cache by calling the {Parse::Client#clear_cache!}
404
+ # method on the client connection. See {https://github.com/minad/moneta Moneta}.
405
+ # @option opts [Integer] :expires Sets the default cache expiration time
406
+ # (in seconds) for successful non-empty GET requests when using the caching
407
+ # middleware. The default value is 3 seconds. If :expires is set to 0,
408
+ # caching will be disabled. You can always clear the current state of the
409
+ # cache using the clear_cache! method on your Parse::Client instance.
410
+ # @option opts [Hash] :faraday You may pass a hash of options that will be
411
+ # passed to the Faraday constructor.
412
+ # @option opts [String] :live_query_url The WebSocket URL for Parse LiveQuery server
413
+ # (e.g., "wss://your-parse-server.com"). If not specified, falls back to
414
+ # ENV["PARSE_LIVE_QUERY_URL"]. LiveQuery enables real-time subscriptions
415
+ # to changes in Parse objects.
416
+ # @example Enable LiveQuery
417
+ # Parse.setup(
418
+ # server_url: "https://your-server.com/parse",
419
+ # application_id: "YOUR_APP_ID",
420
+ # api_key: "YOUR_API_KEY",
421
+ # live_query_url: "wss://your-server.com"
422
+ # )
423
+ # @option opts [Hash] :live_query Advanced LiveQuery configuration options.
424
+ # Pass a hash with custom settings for the LiveQuery client.
425
+ # - :url [String] - WebSocket URL (alternative to :live_query_url)
426
+ # - :auto_reconnect [Boolean] - Auto-reconnect on disconnect (default: true)
427
+ # @raise Parse::Error::ConnectionError if the client was not properly configured with required keys or url.
428
+ # @raise ArgumentError if the cache instance passed to the :cache option is not of Moneta::Transformer or Moneta::Expires
429
+ # @see Parse::Middleware::BodyBuilder
430
+ # @see Parse::Middleware::Caching
431
+ # @see Parse::Middleware::Authentication
432
+ # @see Parse::Protocol
433
+ def initialize(opts = {})
434
+ @server_url = opts[:server_url] || ENV["PARSE_SERVER_URL"] || Parse::Protocol::SERVER_URL
435
+ @application_id = opts[:application_id] || opts[:app_id] || ENV["PARSE_SERVER_APPLICATION_ID"] || ENV["PARSE_APP_ID"]
436
+ @api_key = opts[:api_key] || opts[:rest_api_key] || ENV["PARSE_SERVER_REST_API_KEY"] || ENV["PARSE_API_KEY"]
437
+ @master_key = opts[:master_key] || ENV["PARSE_SERVER_MASTER_KEY"] || ENV["PARSE_MASTER_KEY"]
438
+
439
+ @require_https = opts.fetch(:require_https, ENV["PARSE_REQUIRE_HTTPS"] == "true")
440
+ @allow_faraday_proxy = opts.fetch(:allow_faraday_proxy, false)
441
+
442
+ # Security check for HTTP usage (except localhost/127.0.0.1 for development)
443
+ if @server_url&.start_with?("http://") && !@server_url.match?(%r{^http://(localhost|127\.0\.0\.1)(:|/)})
444
+ if @require_https
445
+ raise ArgumentError, "[Parse::Client] HTTPS required but server URL uses HTTP: #{@server_url}. " \
446
+ "Set require_https: false or use an HTTPS URL."
447
+ else
448
+ warn "[Parse::Client] SECURITY WARNING: Using HTTP instead of HTTPS for Parse server. " \
449
+ "This exposes credentials and data to network interception. " \
450
+ "Use HTTPS in production: #{@server_url}"
451
+ end
452
+ end
453
+
454
+ # Determine the HTTP adapter to use
455
+ # Priority: explicit :adapter > :connection_pooling setting > default (pooling enabled)
456
+ # Falls back to default adapter if net_http_persistent is not available
457
+ if opts[:adapter]
458
+ # User explicitly specified an adapter, use it directly
459
+ adapter = opts[:adapter]
460
+ adapter_options = {}
461
+ elsif opts[:connection_pooling] == false
462
+ # User explicitly disabled connection pooling
463
+ adapter = Faraday.default_adapter
464
+ adapter_options = {}
465
+ elsif opts[:connection_pooling].is_a?(Hash)
466
+ # User provided connection pooling with custom options
467
+ if NET_HTTP_PERSISTENT_AVAILABLE
468
+ adapter = :net_http_persistent
469
+ adapter_options = opts[:connection_pooling]
470
+ else
471
+ adapter = Faraday.default_adapter
472
+ adapter_options = {}
473
+ end
474
+ else
475
+ # Default: use persistent connections for better performance (if available)
476
+ if NET_HTTP_PERSISTENT_AVAILABLE
477
+ adapter = :net_http_persistent
478
+ adapter_options = {}
479
+ else
480
+ adapter = Faraday.default_adapter
481
+ adapter_options = {}
482
+ end
483
+ end
484
+
485
+ opts[:expires] ||= 3
486
+ if @server_url.nil? || @application_id.nil? || (@api_key.nil? && @master_key.nil?)
487
+ raise Parse::Error::ConnectionError, "Please call Parse.setup(server_url:, application_id:, api_key:) to setup a client"
488
+ end
489
+ @server_url += "/" unless @server_url.ends_with?("/")
490
+
491
+ # Resolve timeouts. Defaults guard the calling thread against an
492
+ # unresponsive Parse Server (slowloris, hung dyno) which would
493
+ # otherwise tie up Puma/Sidekiq workers indefinitely.
494
+ open_timeout = opts.fetch(:open_timeout, (ENV["PARSE_OPEN_TIMEOUT"] || 5).to_i)
495
+ read_timeout = opts.fetch(:timeout, (ENV["PARSE_TIMEOUT"] || 30).to_i)
496
+
497
+ #Configure Faraday
498
+ opts[:faraday] ||= {}
499
+ # Guard against silent TLS downgrade or attacker-controlled proxy via
500
+ # opts[:faraday]. The require_https check earlier only inspects the URL
501
+ # scheme; without this guard a caller passing
502
+ # faraday: { ssl: { verify: false }, proxy: "http://attacker" }
503
+ # would neuter TLS verification on an HTTPS connection.
504
+ validate_faraday_opts!(opts[:faraday])
505
+ opts[:faraday].merge!(:url => @server_url)
506
+ @conn = Faraday.new(opts[:faraday]) do |conn|
507
+ # Apply timeouts before any user-supplied middleware sees a request.
508
+ conn.options.timeout = read_timeout if read_timeout > 0
509
+ conn.options.open_timeout = open_timeout if open_timeout > 0
510
+ #conn.request :json
511
+
512
+ # Configure logging if enabled
513
+ if opts[:logging].present?
514
+ # Configure the new structured logging middleware
515
+ Parse::Middleware::Logging.enabled = true
516
+ Parse::Middleware::Logging.logger = opts[:logger] if opts[:logger]
517
+ case opts[:logging]
518
+ when :debug
519
+ Parse::Middleware::Logging.log_level = :debug
520
+ Parse::Middleware::BodyBuilder.logging = true
521
+ when :warn
522
+ Parse::Middleware::Logging.log_level = :warn
523
+ else
524
+ Parse::Middleware::Logging.log_level = :info
525
+ end
526
+ end
527
+
528
+ # This middleware handles sending the proper authentication headers to Parse
529
+ # on each request.
530
+
531
+ # this is the required authentication middleware. Should be the first thing
532
+ # so that other middlewares have access to the env that is being set by
533
+ # this middleware. First added is first to brocess.
534
+ conn.use Parse::Middleware::Authentication,
535
+ application_id: @application_id,
536
+ master_key: @master_key,
537
+ api_key: @api_key
538
+ # Request/response logging middleware (configured via Parse.logging_enabled)
539
+ conn.use Parse::Middleware::Logging
540
+
541
+ # Performance profiling middleware (configured via Parse.profiling_enabled)
542
+ conn.use Parse::Middleware::Profiling
543
+
544
+ # This middleware turns the result from Parse into a Parse::Response object
545
+ # and making sure request that are going out, follow the proper MIME format.
546
+ # We place it after the Authentication middleware in case we need to use then
547
+ # authentication information when building request and responses.
548
+ conn.use Parse::Middleware::BodyBuilder
549
+
550
+ if opts[:cache].present?
551
+ if opts[:expires].to_i <= 0
552
+ warn "[Parse::Client] Cache store provided but :expires is not set or is 0. " \
553
+ "Caching will be disabled. Set :expires to enable caching (e.g., expires: 10)."
554
+ else
555
+ # advanced: provide a REDIS url, we'll configure a Moneta Redis store.
556
+ if opts[:cache].is_a?(String) && opts[:cache].starts_with?("redis://")
557
+ begin
558
+ opts[:cache] = Moneta.new(:Redis, url: opts[:cache])
559
+ rescue LoadError
560
+ puts "[Parse::Middleware::Caching] Did you forget to load the redis gem (Gemfile)?"
561
+ raise
562
+ end
563
+ end
564
+
565
+ unless [:key?, :[], :delete, :store].all? { |method| opts[:cache].respond_to?(method) }
566
+ raise ArgumentError, "Parse::Client option :cache needs to be a type of Moneta store"
567
+ end
568
+ self.cache = opts[:cache]
569
+ conn.use Parse::Middleware::Caching, self.cache, { expires: opts[:expires].to_i }
570
+
571
+ # Inform about opt-in cache behavior
572
+ unless Parse.default_query_cache
573
+ warn "[Parse::Client] Caching middleware enabled (expires: #{opts[:expires]}s). " \
574
+ "Queries do NOT use cache by default. Use `cache: true` on queries to opt-in, " \
575
+ "or set `Parse.default_query_cache = true` for opt-out behavior."
576
+ end
577
+ end
578
+ end
579
+
580
+ yield(conn) if block_given?
581
+
582
+ # Configure the adapter with optional settings
583
+ # For net_http_persistent:
584
+ # - pool_size must be passed as an adapter argument (constructor param, no setter)
585
+ # - idle_timeout and keep_alive have setters and are configured in the block
586
+ if adapter_options.any?
587
+ # Extract constructor arguments for the adapter
588
+ adapter_args = {}
589
+ adapter_args[:pool_size] = adapter_options[:pool_size] if adapter_options[:pool_size]
590
+
591
+ conn.adapter adapter, **adapter_args do |http|
592
+ http.idle_timeout = adapter_options[:idle_timeout] if adapter_options[:idle_timeout]
593
+ http.keep_alive = adapter_options[:keep_alive] if adapter_options[:keep_alive]
594
+ end
595
+ else
596
+ conn.adapter adapter
597
+ end
598
+ end
599
+ # Faraday's constructor may still synthesise a ProxyOptions from
600
+ # HTTPS_PROXY/HTTP_PROXY env vars regardless of the `proxy: nil`
601
+ # we pass in opts. Clear the proxy on the connection itself to be
602
+ # sure no env-derived MITM survives.
603
+ @conn.proxy = nil if !@allow_faraday_proxy && @conn.respond_to?(:proxy=)
604
+ Parse::Client.clients[:default] ||= self
605
+
606
+ # Configure LiveQuery if URL provided
607
+ configure_live_query(opts)
608
+ end
609
+
610
+ # Inspect `opts[:faraday]` for settings that would silently neuter
611
+ # transport security and reject them. Specifically:
612
+ #
613
+ # - `ssl: { verify: false }` on an HTTPS URL — would accept any cert
614
+ # - `proxy: "..."` — would route every request through an attacker-
615
+ # controlled MITM unless explicitly allowlisted
616
+ #
617
+ # @api private
618
+ def validate_faraday_opts!(faraday_opts)
619
+ return unless faraday_opts.is_a?(Hash)
620
+
621
+ ssl = faraday_opts[:ssl] || faraday_opts["ssl"]
622
+ if ssl.is_a?(Hash)
623
+ verify = ssl.key?(:verify) ? ssl[:verify] : ssl["verify"]
624
+ if verify == false && @server_url.to_s.start_with?("https://")
625
+ raise ArgumentError,
626
+ "[Parse::Client] Refusing to disable TLS certificate verification " \
627
+ "(opts[:faraday][:ssl][:verify] = false) on an HTTPS server URL. " \
628
+ "Fix the server certificate or downgrade the URL to http:// " \
629
+ "(with require_https: false) for explicit local testing."
630
+ end
631
+ end
632
+
633
+ proxy = faraday_opts[:proxy] || faraday_opts["proxy"]
634
+ if proxy && !@allow_faraday_proxy
635
+ raise ArgumentError,
636
+ "[Parse::Client] Refusing opts[:faraday][:proxy] = #{proxy.inspect}. " \
637
+ "Routing requests through a proxy can be used to MITM credentials. " \
638
+ "Pass allow_faraday_proxy: true to explicitly opt in."
639
+ end
640
+
641
+ # Suppress Faraday's automatic discovery of HTTPS_PROXY / HTTP_PROXY
642
+ # / NO_PROXY environment variables when the explicit opt-in flag
643
+ # is not set. Without this, a `HTTPS_PROXY` env var leaks every
644
+ # Parse request (and master key) through a process-environment-
645
+ # controlled proxy — a vector that the explicit `proxy:` check
646
+ # above closes but env-discovery silently re-opens. Setting
647
+ # `proxy: nil` is the Faraday-documented way to disable
648
+ # env-proxy autodiscovery.
649
+ faraday_opts[:proxy] = nil unless @allow_faraday_proxy
650
+ end
651
+ private :validate_faraday_opts!
652
+
653
+ # Configure LiveQuery with the given options
654
+ # @param opts [Hash] configuration options
655
+ # @option opts [String] :live_query_url WebSocket URL for LiveQuery server (wss://...)
656
+ # @api private
657
+ def configure_live_query(opts)
658
+ live_query_url = opts[:live_query_url] || ENV["PARSE_LIVE_QUERY_URL"]
659
+
660
+ return unless live_query_url || opts[:live_query]
661
+
662
+ require_relative "live_query"
663
+
664
+ live_query_opts = opts[:live_query].is_a?(Hash) ? opts[:live_query] : {}
665
+
666
+ Parse::LiveQuery.configure(
667
+ url: live_query_url || live_query_opts[:url],
668
+ application_id: @application_id,
669
+ client_key: @api_key,
670
+ master_key: @master_key,
671
+ **live_query_opts,
672
+ )
673
+ end
674
+
675
+ # If set, returns the current retry count for this instance. Otherwise,
676
+ # returns {DEFAULT_RETRIES}. Set to 0 to disable retry mechanism.
677
+ # @return [Integer] the current retry count for this client.
678
+ def retry_limit
679
+ return DEFAULT_RETRIES if @retry_limit.nil?
680
+ @retry_limit
681
+ end
682
+
683
+ # @return [String] the url prefix of the Parse Server url.
684
+ def url_prefix
685
+ @conn.url_prefix
686
+ end
687
+
688
+ # Clear the client cache
689
+ def clear_cache!
690
+ self.cache.clear if self.cache.present?
691
+ end
692
+
693
+ # Send a REST API request to the server. This is the low-level API used for all requests
694
+ # to the Parse server with the provided options. Every request sent to Parse through
695
+ # the client goes through the configured set of middleware that can be modified by applying
696
+ # different headers or specific options.
697
+ # This method supports retrying requests a few times when a {Parse::ServiceUnavailableError}
698
+ # is raised.
699
+ # @param method [Symbol] The method type of the HTTP request (ex. :get, :post).
700
+ # - This parameter can also be a {Parse::Request} object.
701
+ # @param uri [String] the url path. It should not be an absolute url.
702
+ # @param body [Hash] the body of the request.
703
+ # @param query [Hash] the set of url query parameters to use in a GET request.
704
+ # @param headers [Hash] additional headers to apply to this request.
705
+ # @param opts [Hash] a set of options to pass through the middleware stack.
706
+ # - *:cache* [Integer] the number of seconds to cache this specific request.
707
+ # If set to `false`, caching will be disabled completely all together, which means even if
708
+ # a cached response exists, it will not be used.
709
+ # - *:use_master_key* [Boolean] whether this request should send the master key, if
710
+ # it was configured with {Parse.setup}. By default, if a master key was configured,
711
+ # all outgoing requests will contain it in the request header. Default `true`.
712
+ # - *:session_token* [String] The session token to send in this request. This disables
713
+ # sending the master key in the request, and sends this request with the credentials provided by
714
+ # the session_token.
715
+ # - *:retry* [Integer] The number of retrties to perform if the service is unavailable.
716
+ # Set to false to disable the retry mechanism. When performing request retries, the
717
+ # client will sleep for a number of seconds ({Parse::Client::RETRY_DELAY}) between requests.
718
+ # The default value is {Parse::Client::DEFAULT_RETRIES}.
719
+ # @raise Parse::Error::AuthenticationError when HTTP response status is 401 or 403
720
+ # @raise Parse::Error::TimeoutError when HTTP response status is 400 or
721
+ # 408, and the Parse code is 143 or {Parse::Response::ERROR_TIMEOUT}.
722
+ # @raise Parse::Error::ConnectionError when HTTP response status is 404 is not an object not found error.
723
+ # - This will also be raised if after retrying a request a number of times has finally failed.
724
+ # @raise Parse::Error::ProtocolError when HTTP response status is 405 or 406
725
+ # @raise Parse::Error::ServiceUnavailableError when HTTP response status is 500 or 503.
726
+ # - This may also happen when the Parse Server response code is any
727
+ # number less than {Parse::Response::ERROR_SERVICE_UNAVAILABLE}.
728
+ # @raise Parse::Error::ServerError when the Parse response code is less than 100
729
+ # @raise Parse::Error::RequestLimitExceededError when the Parse response code is {Parse::Response::ERROR_EXCEEDED_BURST_LIMIT}.
730
+ # - This usually means you have exceeded the burst limit on requests, which will mean you will be throttled for the
731
+ # next 60 seconds.
732
+ # @raise Parse::Error::InvalidSessionTokenError when the Parse response code is 209.
733
+ # - This means the session token that was sent in the request seems to be invalid.
734
+ # @return [Parse::Response] the response for this request.
735
+ # @see Parse::Middleware::BodyBuilder
736
+ # @see Parse::Middleware::Caching
737
+ # @see Parse::Middleware::Authentication
738
+ # @see Parse::Protocol
739
+ # @see Parse::Request
740
+ def request(method, uri = nil, body: nil, query: nil, headers: nil, opts: {})
741
+ _retry_count ||= self.retry_limit
742
+
743
+ if opts[:retry] == false
744
+ _retry_count = 0
745
+ elsif opts[:retry].to_i > 0
746
+ _retry_count = opts[:retry]
747
+ end
748
+
749
+ headers ||= {}
750
+ # if the first argument is a Parse::Request object, then construct it
751
+ _request = nil
752
+ if method.is_a?(Request)
753
+ _request = method
754
+ method = _request.method
755
+ uri ||= _request.path
756
+ query ||= _request.query
757
+ body ||= _request.body
758
+ headers.merge! _request.headers
759
+ else
760
+ _request = Parse::Request.new(method, uri, body: body, headers: headers, opts: opts)
761
+ end
762
+
763
+ # http method
764
+ method = method.downcase.to_sym
765
+ # set the User-Agent
766
+ headers[USER_AGENT_HEADER] = USER_AGENT_VERSION
767
+
768
+ if opts[:cache] == false
769
+ headers[Parse::Middleware::Caching::CACHE_CONTROL] = "no-cache"
770
+ elsif opts[:cache] == :write_only
771
+ # Write-only mode: skip reading from cache, but still write to cache
772
+ # Useful for fetch!/reload! which want fresh data but should update cache
773
+ headers[Parse::Middleware::Caching::CACHE_WRITE_ONLY] = "true"
774
+ elsif opts[:cache].is_a?(Numeric)
775
+ # specify the cache duration of this request
776
+ headers[Parse::Middleware::Caching::CACHE_EXPIRES_DURATION] = opts[:cache].to_s
777
+ end
778
+
779
+ if opts[:use_master_key] == false
780
+ headers[Parse::Middleware::Authentication::DISABLE_MASTER_KEY] = "true"
781
+ end
782
+
783
+ token = opts[:session_token]
784
+ if token.present?
785
+ token = token.session_token if token.respond_to?(:session_token)
786
+ headers[Parse::Middleware::Authentication::DISABLE_MASTER_KEY] = "true"
787
+ headers[Parse::Protocol::SESSION_TOKEN] = token
788
+ end
789
+
790
+ #if it is a :get request, then use query params, otherwise body.
791
+ params = (method == :get ? query : body) || {}
792
+ # if the path does not start with the '/1/' prefix, then add it to be nice.
793
+ # actually send the request and return the body
794
+ response_env = @conn.send(method, uri, params, headers)
795
+ response = response_env.body
796
+ response.request = _request
797
+
798
+ case response.http_status
799
+ when 401, 403
800
+ Parse::Client._safe_warn("AuthenticationError", response)
801
+ raise Parse::Error::AuthenticationError, response
802
+ when 400, 408
803
+ if response.code == Parse::Response::ERROR_TIMEOUT || response.code == 143 #"net/http: timeout awaiting response headers"
804
+ Parse::Client._safe_warn("TimeoutError", response)
805
+ raise Parse::Error::TimeoutError, response
806
+ end
807
+ when 404
808
+ unless response.object_not_found?
809
+ Parse::Client._safe_warn("ConnectionError", response)
810
+ raise Parse::Error::ConnectionError, response
811
+ end
812
+ when 405, 406
813
+ Parse::Client._safe_warn("ProtocolError", response)
814
+ raise Parse::Error::ProtocolError, response
815
+ when 429 # Request over the throttle limit
816
+ Parse::Client._safe_warn("RequestLimitExceededError", response)
817
+ raise Parse::Error::RequestLimitExceededError, response
818
+ when 500, 503
819
+ Parse::Client._safe_warn("ServiceUnavailableError", response)
820
+ raise Parse::Error::ServiceUnavailableError, response
821
+ end
822
+
823
+ if response.error?
824
+ if response.code <= Parse::Response::ERROR_SERVICE_UNAVAILABLE
825
+ Parse::Client._safe_warn("ServiceUnavailableError", response)
826
+ raise Parse::Error::ServiceUnavailableError, response
827
+ elsif response.code <= 100
828
+ Parse::Client._safe_warn("ServerError", response)
829
+ raise Parse::Error::ServerError, response
830
+ elsif response.code == Parse::Response::ERROR_EXCEEDED_BURST_LIMIT
831
+ Parse::Client._safe_warn("RequestLimitExceededError", response)
832
+ raise Parse::Error::RequestLimitExceededError, response
833
+ elsif response.code == 209 # Error 209: invalid session token
834
+ Parse::Client._safe_warn("InvalidSessionTokenError", response)
835
+ raise Parse::Error::InvalidSessionTokenError, response
836
+ end
837
+ end
838
+
839
+ response
840
+ rescue Parse::Error::RequestLimitExceededError, Parse::Error::ServiceUnavailableError => e
841
+ if _retry_count > 0
842
+ warn "[Parse:Retry] Retries remaining #{_retry_count} : #{response.request}"
843
+ _retry_count -= 1
844
+ # Use Retry-After header if available, otherwise use exponential backoff
845
+ retry_after = response.retry_after if response.respond_to?(:retry_after)
846
+ if retry_after && retry_after > 0
847
+ _retry_delay = retry_after
848
+ warn "[Parse:Retry] Using Retry-After header: #{_retry_delay}s"
849
+ else
850
+ # Deterministic exponential backoff with +/-25% jitter. Never zero —
851
+ # zero-wait retries amplify DoS against upstream and stampede on 429.
852
+ backoff_delay = RETRY_DELAY * (self.retry_limit - _retry_count)
853
+ _retry_delay = backoff_delay * (0.75 + rand * 0.5)
854
+ end
855
+ sleep _retry_delay if _retry_delay > 0
856
+ retry
857
+ end
858
+ raise
859
+ rescue Faraday::ClientError, Net::OpenTimeout => e
860
+ if _retry_count > 0
861
+ warn "[Parse:Retry] Retries remaining #{_retry_count} : #{_request}"
862
+ _retry_count -= 1
863
+ backoff_delay = RETRY_DELAY * (self.retry_limit - _retry_count)
864
+ _retry_delay = backoff_delay * (0.75 + rand * 0.5)
865
+ sleep _retry_delay if _retry_delay > 0
866
+ retry
867
+ end
868
+ raise Parse::Error::ConnectionError, "#{_request} : #{e.class} - #{e.message}"
869
+ end
870
+
871
+ # Send a GET request.
872
+ # @param uri [String] the uri path for this request.
873
+ # @param query [Hash] the set of url query parameters.
874
+ # @param headers [Hash] additional headers to send in this request.
875
+ # @return (see #request)
876
+ def get(uri, query = nil, headers = {})
877
+ request :get, uri, query: query, headers: headers
878
+ end
879
+
880
+ # Send a POST request.
881
+ # @param uri (see #get)
882
+ # @param body [Hash] a hash that will be JSON encoded for the body of this request.
883
+ # @param headers (see #get)
884
+ # @return (see #request)
885
+ def post(uri, body = nil, headers = {})
886
+ request :post, uri, body: body, headers: headers
887
+ end
888
+
889
+ # Send a PUT request.
890
+ # @param uri (see #post)
891
+ # @param body (see #post)
892
+ # @param headers (see #post)
893
+ # @return (see #request)
894
+ def put(uri, body = nil, headers = {})
895
+ request :put, uri, body: body, headers: headers
896
+ end
897
+
898
+ # Send a DELETE request.
899
+ # @param uri (see #post)
900
+ # @param body (see #post)
901
+ # @param headers (see #post)
902
+ # @return (see #request)
903
+ def delete(uri, body = nil, headers = {})
904
+ request :delete, uri, body: body, headers: headers
905
+ end
906
+
907
+ # Send a {Parse::Request} object.
908
+ # @param req [Parse::Request] the request to send
909
+ # @raise ArgumentError if req is not of type Parse::Request.
910
+ # @return (see #request)
911
+ def send_request(req) #Parse::Request object
912
+ raise ArgumentError, "Object not of Parse::Request type." unless req.is_a?(Parse::Request)
913
+ request req.method, req.path, req.body, req.headers
914
+ end
915
+
916
+ # The connectable module adds methods to objects so that they can get a default
917
+ # Parse::Client object if needed. This is mainly used for Parse::Query and Parse::Object classes.
918
+ # This is included in the Parse::Model class.
919
+ # Any subclass can override their `client` methods to provide a different session to use
920
+ module Connectable
921
+
922
+ # @!visibility private
923
+ def self.included(baseClass)
924
+ baseClass.extend ClassMethods
925
+ end
926
+ # Class methods to be added to any object that wants to have standard access to
927
+ # a the default {Parse::Client} instance.
928
+ module ClassMethods
929
+
930
+ # @return [Parse::Client] the current client for :default.
931
+ attr_writer :client
932
+
933
+ def client
934
+ @client ||= Parse::Client.client #defaults to :default tag
935
+ end
936
+ end
937
+
938
+ # @return [Parse::Client] the current client defined for the class.
939
+ def client
940
+ self.class.client
941
+ end
942
+ end #Connectable
943
+ end
944
+
945
+ # Helper method that users should call to setup the client stack.
946
+ # A block can be passed in order to do additional client configuration.
947
+ # To connect to a Parse server, you will need a minimum of an application_id,
948
+ # an api_key and a server_url. To connect to the server endpoint, you use the
949
+ # {Parse.setup} method below.
950
+ #
951
+ # @example (see Parse::Client.setup)
952
+ # @param opts (see Parse::Client.setup)
953
+ # @option opts (see Parse::Client.setup)
954
+ # @yield (see Parse::Client.setup)
955
+ # @return (see Parse::Client.setup)
956
+ # @see Parse::Client.setup
957
+ def self.setup(opts = {}, &block)
958
+ if block_given?
959
+ Parse::Client.new(opts, &block)
960
+ else
961
+ Parse::Client.new(opts)
962
+ end
963
+ end
964
+
965
+ # @!visibility private
966
+ # Unwrap the `{ "result" => ... }` envelope from a successful cloud-code response.
967
+ # Guards against unusual server payloads (non-Hash bodies) by returning the raw
968
+ # result rather than raising TypeError on `String#[]`/`Integer#[]`.
969
+ def self._extract_cloud_result(response)
970
+ r = response.result
971
+ r.is_a?(Hash) ? r["result"] : r
972
+ end
973
+
974
+ # Helper method to trigger cloud jobs and get results.
975
+ # @param name [String] the name of the cloud code job to trigger.
976
+ # @param body [Hash] the set of parameters to pass to the job.
977
+ # @param opts (see Parse.call_function)
978
+ # @return (see Parse.call_function)
979
+ def self.trigger_job(name, body = {}, **opts)
980
+ conn = opts[:session] || opts[:client] || :default
981
+
982
+ # Extract request options for the API call
983
+ request_opts = {}
984
+ request_opts[:session_token] = opts[:session_token] if opts[:session_token]
985
+ request_opts[:master_key] = opts[:master_key] if opts[:master_key]
986
+
987
+ response = Parse::Client.client(conn).trigger_job(name, body, opts: request_opts)
988
+ return response if opts[:raw].present?
989
+ if response.error?
990
+ Parse::Client._safe_warn("CloudCodeError", response, name: name)
991
+ return nil
992
+ end
993
+ _extract_cloud_result(response)
994
+ end
995
+
996
+ # Same as {Parse.trigger_job} but raises {Parse::Error::CloudCodeError} when
997
+ # the job returns an error instead of silently returning nil. HTTP-level
998
+ # errors (auth, timeouts, throttling, etc.) still raise their specific
999
+ # {Parse::Error} subclasses as the underlying client does.
1000
+ # @param name (see Parse.trigger_job)
1001
+ # @param body (see Parse.trigger_job)
1002
+ # @param opts (see Parse.trigger_job) — :raw is ignored.
1003
+ # @raise [Parse::Error::CloudCodeError] when the response indicates a cloud-code error.
1004
+ # @return [Object] the result data of the response.
1005
+ def self.trigger_job!(name, body = {}, **opts)
1006
+ response = trigger_job(name, body, **opts.merge(raw: true))
1007
+ raise Parse::Error::CloudCodeError.new(name, response) if response.error?
1008
+ _extract_cloud_result(response)
1009
+ end
1010
+
1011
+ # Helper method to trigger cloud jobs with a session token.
1012
+ # This is a convenience method that ensures proper session token handling.
1013
+ # @param name [String] the name of the cloud code job to trigger.
1014
+ # @param body [Hash] the set of parameters to pass to the job.
1015
+ # @param session_token [String] the session token for authenticated requests.
1016
+ # @param opts [Hash] additional options (same as trigger_job).
1017
+ # @return [Object] the result data of the response. nil if there was an error.
1018
+ def self.trigger_job_with_session(name, body = {}, session_token, **opts)
1019
+ opts[:session_token] = session_token
1020
+ trigger_job(name, body, **opts)
1021
+ end
1022
+
1023
+ # Same as {Parse.trigger_job_with_session} but raises
1024
+ # {Parse::Error::CloudCodeError} when the job returns an error instead of
1025
+ # silently returning nil.
1026
+ # @param name (see Parse.trigger_job_with_session)
1027
+ # @param body (see Parse.trigger_job_with_session)
1028
+ # @param session_token (see Parse.trigger_job_with_session)
1029
+ # @param opts (see Parse.trigger_job_with_session)
1030
+ # @raise [Parse::Error::CloudCodeError] when the response indicates a cloud-code error.
1031
+ # @return [Object] the result data of the response.
1032
+ def self.trigger_job_with_session!(name, body = {}, session_token, **opts)
1033
+ opts[:session_token] = session_token
1034
+ trigger_job!(name, body, **opts)
1035
+ end
1036
+
1037
+ # Helper method to call cloud functions and get results.
1038
+ # @param name [String] the name of the cloud code function to call.
1039
+ # @param body [Hash] the set of parameters to pass to the function.
1040
+ # @param opts [Hash] additional options.
1041
+ # @option opts [String] :session_token The session token for authenticated requests.
1042
+ # @option opts [Symbol] :session The client connection to use (alternative to :client).
1043
+ # @option opts [Symbol] :client The client connection to use.
1044
+ # @option opts [Boolean] :raw Whether to return the raw response object.
1045
+ # @option opts [Boolean] :master_key Whether to use the master key for this request.
1046
+ # @return [Object] the result data of the response. nil if there was an error.
1047
+ def self.call_function(name, body = {}, **opts)
1048
+ conn = opts[:session] || opts[:client] || :default
1049
+
1050
+ # Extract request options for the API call
1051
+ request_opts = {}
1052
+ request_opts[:session_token] = opts[:session_token] if opts[:session_token]
1053
+ request_opts[:master_key] = opts[:master_key] if opts[:master_key]
1054
+
1055
+ response = Parse::Client.client(conn).call_function(name, body, opts: request_opts)
1056
+ return response if opts[:raw].present?
1057
+ if response.error?
1058
+ Parse::Client._safe_warn("CloudCodeError", response, name: name)
1059
+ return nil
1060
+ end
1061
+ _extract_cloud_result(response)
1062
+ end
1063
+
1064
+ # Same as {Parse.call_function} but raises {Parse::Error::CloudCodeError}
1065
+ # when the cloud function returns an error instead of silently returning nil.
1066
+ # HTTP-level errors (auth, timeouts, throttling, etc.) still raise their
1067
+ # specific {Parse::Error} subclasses as the underlying client does.
1068
+ # @param name (see Parse.call_function)
1069
+ # @param body (see Parse.call_function)
1070
+ # @param opts (see Parse.call_function) — :raw is ignored.
1071
+ # @raise [Parse::Error::CloudCodeError] when the response indicates a cloud-code error.
1072
+ # @return [Object] the result data of the response.
1073
+ def self.call_function!(name, body = {}, **opts)
1074
+ response = call_function(name, body, **opts.merge(raw: true))
1075
+ raise Parse::Error::CloudCodeError.new(name, response) if response.error?
1076
+ _extract_cloud_result(response)
1077
+ end
1078
+
1079
+ # Helper method to call cloud functions with a session token.
1080
+ # This is a convenience method that ensures proper session token handling.
1081
+ # @param name [String] the name of the cloud code function to call.
1082
+ # @param body [Hash] the set of parameters to pass to the function.
1083
+ # @param session_token [String] the session token for authenticated requests.
1084
+ # @param opts [Hash] additional options (same as call_function).
1085
+ # @return [Object] the result data of the response. nil if there was an error.
1086
+ def self.call_function_with_session(name, body = {}, session_token, **opts)
1087
+ opts[:session_token] = session_token
1088
+ call_function(name, body, **opts)
1089
+ end
1090
+
1091
+ # Same as {Parse.call_function_with_session} but raises
1092
+ # {Parse::Error::CloudCodeError} when the cloud function returns an error
1093
+ # instead of silently returning nil.
1094
+ # @param name (see Parse.call_function_with_session)
1095
+ # @param body (see Parse.call_function_with_session)
1096
+ # @param session_token (see Parse.call_function_with_session)
1097
+ # @param opts (see Parse.call_function_with_session)
1098
+ # @raise [Parse::Error::CloudCodeError] when the response indicates a cloud-code error.
1099
+ # @return [Object] the result data of the response.
1100
+ def self.call_function_with_session!(name, body = {}, session_token, **opts)
1101
+ opts[:session_token] = session_token
1102
+ call_function!(name, body, **opts)
1103
+ end
1104
+ end