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,91 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ module Parse
5
+ # Set of Parse protocol constants.
6
+ module Protocol
7
+ # The default server url, based on the hosted Parse platform.
8
+ # Uses HTTPS by default for security. Override with ENV["PARSE_SERVER_URL"]
9
+ # or pass server_url: to Parse.setup for custom configurations.
10
+ SERVER_URL = "https://localhost:1337/parse".freeze
11
+ # The request header field to send the application Id.
12
+ APP_ID = "X-Parse-Application-Id"
13
+ # The request header field to send the REST API key.
14
+ API_KEY = "X-Parse-REST-API-Key"
15
+ # The request header field to send the Master key.
16
+ MASTER_KEY = "X-Parse-Master-Key"
17
+ # The request header field to send the revocable Session key.
18
+ SESSION_TOKEN = "X-Parse-Session-Token"
19
+ # The request header field to request a revocable session token.
20
+ REVOCABLE_SESSION = "X-Parse-Revocable-Session"
21
+ # The request header field to send the installation id.
22
+ INSTALLATION_ID = "Parse-Installation-Id"
23
+ # The request header field to send an email when authenticating with Parse hosted platform.
24
+ EMAIL = "X-Parse-Email"
25
+ # The request header field to send the password when authenticating with the Parse hosted platform.
26
+ PASSWORD = "X-Parse-Password"
27
+ # The request header field for the Content type.
28
+ CONTENT_TYPE = "Content-Type"
29
+ # The default content type format for sending API requests.
30
+ CONTENT_TYPE_FORMAT = "application/json; charset=utf-8"
31
+ # The request header field for MongoDB read preference.
32
+ # Supported values: PRIMARY, PRIMARY_PREFERRED, SECONDARY, SECONDARY_PREFERRED, NEAREST
33
+ READ_PREFERENCE = "X-Parse-Read-Preference"
34
+
35
+ # Valid read preference values for MongoDB
36
+ READ_PREFERENCES = %w[PRIMARY PRIMARY_PREFERRED SECONDARY SECONDARY_PREFERRED NEAREST].freeze
37
+ end
38
+
39
+ # All Parse error codes.
40
+ # @todo Implement all error codes as StandardError
41
+ #
42
+ # List of error codes.
43
+ # OtherCause -1 Error code indicating that an unknown error or an error unrelated to Parse occurred.
44
+ # InternalServerError 1 Error code indicating that something has gone wrong with the server. If you get this error code, it is Parse's fault. Please report the bug to https://parse.com/help.
45
+ # ConnectionFailed 100 Error code indicating the connection to the Parse servers failed.
46
+ # ObjectNotFound 101 Error code indicating the specified object doesn't exist.
47
+ # InvalidQuery 102 Error code indicating you tried to query with a datatype that doesn't support it, like exact matching an array or object.
48
+ # InvalidClassName 103 Error code indicating a missing or invalid classname. Classnames are case-sensitive. They must start with a letter, and a-zA-Z0-9_ are the only valid characters.
49
+ # MissingObjectId 104 Error code indicating an unspecified object id.
50
+ # InvalidKeyName 105 Error code indicating an invalid key name. Keys are case-sensitive. They must start with a letter, and a-zA-Z0-9_ are the only valid characters.
51
+ # InvalidPointer 106 Error code indicating a malformed pointer. You should not see this unless you have been mucking about changing internal Parse code.
52
+ # InvalidJSON 107 Error code indicating that badly formed JSON was received upstream. This either indicates you have done something unusual with modifying how things encode to JSON, or the network is failing badly.
53
+ # CommandUnavailable 108 Error code indicating that the feature you tried to access is only available internally for testing purposes.
54
+ # NotInitialized 109 You must call Parse.initialize before using the Parse library.
55
+ # IncorrectType 111 Error code indicating that a field was set to an inconsistent type.
56
+ # InvalidChannelName 112 Error code indicating an invalid channel name. A channel name is either an empty string (the broadcast channel) or contains only a-zA-Z0-9_ characters and starts with a letter.
57
+ # PushMisconfigured 115 Error code indicating that push is misconfigured.
58
+ # ObjectTooLarge 116 Error code indicating that the object is too large.
59
+ # OperationForbidden 119 Error code indicating that the operation isn't allowed for clients.
60
+ # CacheMiss 120 Error code indicating the result was not found in the cache.
61
+ # InvalidNestedKey 121 Error code indicating that an invalid key was used in a nested JSONObject.
62
+ # InvalidFileName 122 Error code indicating that an invalid filename was used for ParseFile. A valid file name contains only a-zA-Z0-9_. characters and is between 1 and 128 characters.
63
+ # InvalidACL 123 Error code indicating an invalid ACL was provided.
64
+ # Timeout 124 Error code indicating that the request timed out on the server. Typically this indicates that the request is too expensive to run.
65
+ # InvalidEmailAddress 125 Error code indicating that the email address was invalid.
66
+ # DuplicateValue 137 Error code indicating that a unique field was given a value that is already taken.
67
+ # InvalidRoleName 139 Error code indicating that a role's name is invalid.
68
+ # ExceededQuota 140 Error code indicating that an application quota was exceeded. Upgrade to resolve.
69
+ # ScriptFailed 141 Error code indicating that a Cloud Code script failed.
70
+ # ValidationFailed 142 Error code indicating that a Cloud Code validation failed.
71
+ # FileDeleteFailed 153 Error code indicating that deleting a file failed.
72
+ # RequestLimitExceeded 155 Error code indicating that the application has exceeded its request limit.
73
+ # InvalidEventName 160 Error code indicating that the provided event name is invalid.
74
+ # UsernameMissing 200 Error code indicating that the username is missing or empty.
75
+ # PasswordMissing 201 Error code indicating that the password is missing or empty.
76
+ # UsernameTaken 202 Error code indicating that the username has already been taken.
77
+ # EmailTaken 203 Error code indicating that the email has already been taken.
78
+ # EmailMissing 204 Error code indicating that the email is missing, but must be specified.
79
+ # EmailNotFound 205 Error code indicating that a user with the specified email was not found.
80
+ # SessionMissing 206 Error code indicating that a user object without a valid session could not be altered.
81
+ # MustCreateUserThroughSignup 207 Error code indicating that a user can only be created through signup.
82
+ # AccountAlreadyLinked 208 Error code indicating that an an account being linked is already linked to another user.
83
+ # InvalidSessionToken 209 Error code indicating that the current session token is invalid.
84
+ # LinkedIdMissing 250 Error code indicating that a user cannot be linked to an account because that account's id could not be found.
85
+ # InvalidLinkedSession 251 Error code indicating that a user with a linked (e.g. Facebook) account has an invalid session.
86
+ # UnsupportedService 252 Error code indicating that a service being linked (e.g. Facebook or Twitter) is unsupported.
87
+ # module ErrorCodes
88
+ #
89
+ # end
90
+
91
+ end
@@ -0,0 +1,233 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ require "active_support"
5
+ require "active_support/json"
6
+ require "securerandom"
7
+
8
+ module Parse
9
+ #This class represents a Parse request.
10
+ class Request
11
+ # @!attribute [rw] method
12
+ # @return [String] the HTTP method used for this request.
13
+
14
+ # @!attribute [rw] path
15
+ # @return [String] the uri path.
16
+
17
+ # @!attribute [rw] body
18
+ # @return [Hash] the body of this request.
19
+
20
+ # TODO: Document opts and cache options.
21
+
22
+ # @!attribute [rw] opts
23
+ # @return [Hash] a set of options for this request.
24
+ # @!attribute [rw] cache
25
+ # @return [Boolean]
26
+ attr_accessor :method, :path, :body, :headers, :opts, :cache
27
+
28
+ # @!visibility private
29
+ # Used to correlate batching requests with their responses.
30
+ attr_accessor :tag
31
+
32
+ # @!attribute [rw] request_id
33
+ # @return [String] unique identifier for this request to enable idempotency
34
+ attr_accessor :request_id
35
+
36
+ # Class-level configuration for request ID behavior
37
+ class << self
38
+ # @!attribute [rw] enable_request_id
39
+ # @return [Boolean] whether to automatically generate request IDs for idempotency
40
+ attr_accessor :enable_request_id
41
+
42
+ # @!attribute [rw] request_id_header
43
+ # @return [String] the header name to use for request IDs
44
+ attr_accessor :request_id_header
45
+
46
+ # @!attribute [rw] idempotent_methods
47
+ # @return [Array<Symbol>] HTTP methods that should include request IDs
48
+ attr_accessor :idempotent_methods
49
+ end
50
+
51
+ # Default configuration
52
+ self.enable_request_id = true # Enabled by default for production safety
53
+ self.request_id_header = "X-Parse-Request-Id" # Standard Parse header
54
+ self.idempotent_methods = [:post, :put, :patch] # Methods that can benefit from idempotency
55
+
56
+ # Creates a new request
57
+ # @param method [String] the HTTP method
58
+ # @param uri [String] the API path of the request (without the host)
59
+ # @param body [Hash] the body (or parameters) of this request.
60
+ # @param headers [Hash] additional headers to send in this request.
61
+ # @param opts [Hash] additional optional parameters.
62
+ # @option opts [String] :request_id custom request ID for idempotency
63
+ # @option opts [Boolean] :idempotent force enable/disable idempotency for this request
64
+ def initialize(method, uri, body: nil, headers: nil, opts: {})
65
+ @tag = 0
66
+ method = method.downcase.to_sym
67
+ unless method == :get || method == :put || method == :post || method == :delete
68
+ raise ArgumentError, "Invalid method #{method} for request : '#{uri}'"
69
+ end
70
+
71
+ self.method = method
72
+ self.path = uri
73
+ self.body = body
74
+ self.headers = headers || {}
75
+ self.opts = opts || {}
76
+
77
+ # Handle request ID for idempotency
78
+ setup_request_id
79
+ end
80
+
81
+ # The parameters of this request if the HTTP method is GET.
82
+ # @return [Hash]
83
+ def query
84
+ body if @method == :get
85
+ end
86
+
87
+ # @return [Hash] JSON encoded hash
88
+ def as_json
89
+ signature.as_json
90
+ end
91
+
92
+ # @return [Boolean]
93
+ def ==(r)
94
+ return false unless r.is_a?(Request)
95
+ @method == r.method && @path == r.path && @body == r.body && @headers == r.headers
96
+ end
97
+
98
+ # Signature provies a way for us to compare different requests objects.
99
+ # Two requests objects are the same if they have the same signature.
100
+ # @return [Hash] A hash representing this request.
101
+ def signature
102
+ { method: @method.upcase, path: @path, body: @body }
103
+ end
104
+
105
+ # @!visibility private
106
+ def inspect
107
+ "#<#{self.class} @method=#{@method} @path='#{@path}'>"
108
+ end
109
+
110
+ # @return [String]
111
+ def to_s
112
+ "#{@method.to_s.upcase} #{@path}"
113
+ end
114
+
115
+ private
116
+
117
+ # Sets up request ID for idempotency based on configuration and request properties
118
+ def setup_request_id
119
+ # Check if idempotency should be enabled for this request
120
+ should_use_request_id = determine_idempotency_requirement
121
+
122
+ return unless should_use_request_id
123
+
124
+ # Use custom request ID if provided, otherwise generate one
125
+ @request_id = @opts[:request_id] || generate_request_id
126
+
127
+ # Add request ID to headers if not already present
128
+ header_name = self.class.request_id_header
129
+ @headers[header_name] ||= @request_id
130
+ end
131
+
132
+ # Determines if this request should use a request ID for idempotency
133
+ # @return [Boolean]
134
+ def determine_idempotency_requirement
135
+ # Explicit override in opts takes precedence
136
+ return @opts[:idempotent] if @opts.key?(:idempotent)
137
+
138
+ # Check if request ID is already in headers (manually added)
139
+ return true if @headers[self.class.request_id_header]
140
+
141
+ # Check global configuration and method
142
+ return false unless self.class.enable_request_id
143
+ return false unless self.class.idempotent_methods.include?(@method)
144
+
145
+ # Don't add request IDs to certain paths that are inherently idempotent
146
+ # or where Parse handles idempotency differently
147
+ return false if non_idempotent_path?
148
+
149
+ true
150
+ end
151
+
152
+ # Checks if the request path should not use request IDs
153
+ # @return [Boolean]
154
+ def non_idempotent_path?
155
+ # GET requests are naturally idempotent
156
+ return true if @method == :get
157
+
158
+ # Some Parse endpoints handle their own idempotency or shouldn't be retried
159
+ non_idempotent_patterns = [
160
+ %r{/sessions}, # Session creation/management
161
+ %r{/logout}, # Logout operations
162
+ %r{/requestPasswordReset}, # Password reset requests
163
+ %r{/functions/}, # Cloud functions (may have their own logic)
164
+ %r{/jobs/}, # Background jobs
165
+ %r{/events/}, # Analytics events
166
+ %r{/push}, # Push notifications
167
+ ]
168
+
169
+ non_idempotent_patterns.any? { |pattern| @path =~ pattern }
170
+ end
171
+
172
+ # Generates a unique request ID
173
+ # @return [String] a unique identifier for this request
174
+ def generate_request_id
175
+ # Use a format that identifies the request came from Ruby Parse Stack
176
+ # and includes a UUID for uniqueness
177
+ "_RB_#{SecureRandom.uuid}"
178
+ end
179
+
180
+ public
181
+
182
+ # Enables idempotency for this specific request
183
+ # @param custom_id [String] optional custom request ID to use
184
+ # @return [self] for method chaining
185
+ def with_idempotency(custom_id = nil)
186
+ @opts[:idempotent] = true
187
+ @opts[:request_id] = custom_id if custom_id
188
+ setup_request_id
189
+ self
190
+ end
191
+
192
+ # Disables idempotency for this specific request
193
+ # @return [self] for method chaining
194
+ def without_idempotency
195
+ @opts[:idempotent] = false
196
+ @request_id = nil
197
+ @headers.delete(self.class.request_id_header)
198
+ self
199
+ end
200
+
201
+ # Checks if this request has idempotency enabled
202
+ # @return [Boolean]
203
+ def idempotent?
204
+ @request_id.present? && @headers[self.class.request_id_header].present?
205
+ end
206
+
207
+ # Class methods for configuration
208
+
209
+ # Enables request ID generation globally
210
+ # @param methods [Array<Symbol>] HTTP methods to apply idempotency to
211
+ # @param header [String] header name to use for request IDs
212
+ def self.enable_idempotency!(methods: [:post, :put, :patch], header: "X-Parse-Request-Id")
213
+ self.enable_request_id = true
214
+ self.idempotent_methods = methods
215
+ self.request_id_header = header
216
+ end
217
+
218
+ # Disables request ID generation globally
219
+ def self.disable_idempotency!
220
+ self.enable_request_id = false
221
+ end
222
+
223
+ # Configures idempotency settings
224
+ # @param enabled [Boolean] whether to enable idempotency
225
+ # @param methods [Array<Symbol>] HTTP methods to apply idempotency to
226
+ # @param header [String] header name to use for request IDs
227
+ def self.configure_idempotency(enabled: true, methods: [:post, :put, :patch], header: "X-Parse-Request-Id")
228
+ self.enable_request_id = enabled
229
+ self.idempotent_methods = methods
230
+ self.request_id_header = header
231
+ end
232
+ end
233
+ end
@@ -0,0 +1,208 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ require "active_support"
5
+ require "active_support/json"
6
+
7
+ module Parse
8
+
9
+ # Represents a response from Parse server. A response can also
10
+ # be a set of responses (from a Batch response).
11
+ class Response
12
+ include Enumerable
13
+
14
+ # Code for an unknown error.
15
+ ERROR_INTERNAL = 1
16
+ # Code when the server returns a 500 or is non-responsive.
17
+ ERROR_SERVICE_UNAVAILABLE = 2
18
+ # Code when the request times out.
19
+ ERROR_TIMEOUT = 124
20
+ # Code when the requests per second limit as been exceeded.
21
+ ERROR_EXCEEDED_BURST_LIMIT = 155
22
+ # Code when a requested record is not found.
23
+ ERROR_OBJECT_NOT_FOUND = 101
24
+ # Code when the username is missing in request.
25
+ ERROR_USERNAME_MISSING = 200
26
+ # Code when the password is missing in request.
27
+ ERROR_PASSWORD_MISSING = 201
28
+ # Code when the username is already in the system.
29
+ ERROR_USERNAME_TAKEN = 202
30
+ # Code when the email is already in the system.
31
+ ERROR_EMAIL_TAKEN = 203
32
+ # Code when the email is not found
33
+ ERROR_EMAIL_NOT_FOUND = 205
34
+ # Code when the email is invalid
35
+ ERROR_EMAIL_INVALID = 125
36
+
37
+ # The field name for the error.
38
+ ERROR = "error".freeze
39
+ # The field name for the success.
40
+ SUCCESS = "success".freeze
41
+ # The field name for the error code.
42
+ CODE = "code".freeze
43
+ # The field name for the results of the request.
44
+ RESULTS = "results".freeze
45
+ # The field name for the count result in a count response.
46
+ COUNT = "count".freeze
47
+
48
+ # The Retry-After header name.
49
+ RETRY_AFTER = "Retry-After".freeze
50
+
51
+ # @!attribute [rw] parse_class
52
+ # @return [String] the Parse class for this request
53
+ # @!attribute [rw] code
54
+ # @return [Integer] the error code
55
+ # @!attribute [rw] error
56
+ # @return [Integer] the error message
57
+ # @!attribute [rw] result
58
+ # @return [Hash] the body of the response result.
59
+ # @!attribute [rw] http_status
60
+ # @return [Integer] the HTTP status code from the response.
61
+ # @!attribute [rw] request
62
+ # @return [Integer] the Parse::Request that generated this response.
63
+ # @see Parse::Request
64
+ # @!attribute [rw] headers
65
+ # @return [Hash] the HTTP response headers.
66
+ attr_accessor :parse_class, :code, :error, :result, :http_status,
67
+ :request, :headers
68
+ # You can query Parse for counting objects, which may not actually have
69
+ # results.
70
+ # @return [Integer] the count result from a count query request.
71
+ attr_reader :count
72
+
73
+ # Get the Retry-After header value as seconds.
74
+ # The Retry-After header can be either a number of seconds or an HTTP-date.
75
+ # @return [Integer, nil] seconds to wait before retrying, or nil if not present.
76
+ def retry_after
77
+ return nil unless @headers.is_a?(Hash)
78
+ value = @headers[RETRY_AFTER] || @headers["retry-after"]
79
+ return nil if value.nil?
80
+
81
+ # Try parsing as integer (seconds)
82
+ if value.to_s =~ /\A\d+\z/
83
+ value.to_i
84
+ else
85
+ # Try parsing as HTTP-date
86
+ begin
87
+ date = Time.httpdate(value.to_s)
88
+ delay = (date - Time.now).ceil
89
+ delay > 0 ? delay : 1
90
+ rescue ArgumentError
91
+ nil
92
+ end
93
+ end
94
+ end
95
+
96
+ # Create an instance with a Parse response JSON hash.
97
+ # @param res [Hash] the JSON hash
98
+ def initialize(res = {})
99
+ @http_status = 0
100
+ @count = 0
101
+ @batch_response = false # by default, not a batch response
102
+ @result = nil
103
+ # If a string is used for initializing, treat it as JSON
104
+ # check for string to not be 'OK' since that is the health check API response
105
+ res = JSON.parse(res) if res.is_a?(String) && res != "OK".freeze
106
+ # If it is a hash (or parsed JSON), then parse the result.
107
+ parse_result!(res) if res.is_a?(Hash)
108
+ # if the result is an Array, then most likely it is a set of responses
109
+ # from using a Batch API.
110
+ if res.is_a?(Array)
111
+ @batch_response = true
112
+ @result = res || []
113
+ @count = @result.count
114
+ end
115
+ #if none match, set pure result
116
+ @result = res if @result.nil?
117
+ end
118
+
119
+ # true if this was a batch response.
120
+ def batch?
121
+ @batch_response
122
+ end
123
+
124
+ # If it is a batch respnose, we'll create an array of Response objects for each
125
+ # of the ones in the batch.
126
+ # @return [Array] an array of Response objects.
127
+ def batch_responses
128
+ return [@result] unless @batch_response
129
+ # if batch response, generate array based on the response hash.
130
+ @result.map do |r|
131
+ next r unless r.is_a?(Hash)
132
+ hash = r[SUCCESS] || r[ERROR]
133
+ Parse::Response.new hash
134
+ end
135
+ end
136
+
137
+ # This method takes the result hash and determines if it is a regular
138
+ # parse query result, object result or a count result. The response should
139
+ # be a hash either containing the result data or the error.
140
+ def parse_result!(h)
141
+ @result = {}
142
+ return unless h.is_a?(Hash)
143
+ @code = h[CODE]
144
+ @error = h[ERROR]
145
+ if h[RESULTS].is_a?(Array)
146
+ @result = h[RESULTS]
147
+ @count = h[COUNT] || @result.count
148
+ else
149
+ @result = h
150
+ @count = 1
151
+ end
152
+ end
153
+
154
+ alias_method :parse_results!, :parse_result!
155
+
156
+ # true if the response is successful.
157
+ # @see #error?
158
+ def success?
159
+ @code.nil? && @error.nil?
160
+ end
161
+
162
+ # true if the response has an error code.
163
+ # @see #success?
164
+ def error?
165
+ !success?
166
+ end
167
+
168
+ # true if the response has an error code of 'object not found'
169
+ # @see ERROR_OBJECT_NOT_FOUND
170
+ def object_not_found?
171
+ @code == ERROR_OBJECT_NOT_FOUND
172
+ end
173
+
174
+ # @return [Array] the result data from the response.
175
+ def results
176
+ return [] if @result.nil?
177
+ @result.is_a?(Array) ? @result : [@result]
178
+ end
179
+
180
+ # @return [Object] the first thing in the result array.
181
+ def first
182
+ @result.is_a?(Array) ? @result.first : @result
183
+ end
184
+
185
+ # Iterate through each result item.
186
+ # @yieldparam [Object] a result entry.
187
+ def each(&block)
188
+ return enum_for(:each) unless block_given?
189
+ results.each(&block)
190
+ self
191
+ end
192
+
193
+ # @!visibility private
194
+ def inspect
195
+ if error?
196
+ "#<#{self.class} @code=#{code} @error='#{error}'>"
197
+ else
198
+ "#<#{self.class} @result='#{@result}'>"
199
+ end
200
+ end
201
+
202
+ # @return [String] JSON encoded object, or an error string.
203
+ def to_s
204
+ return "[E-#{@code}] #{@request} : #{@error} (#{@http_status})" if error?
205
+ @result.to_json
206
+ end
207
+ end
208
+ end