parse-stack-next 4.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.bundle/config +2 -0
- data/.env.sample +112 -0
- data/.env.test +10 -0
- data/.github/workflows/ruby.yml +36 -0
- data/.gitignore +49 -0
- data/.ruby-version +1 -0
- data/.solargraph.yml +22 -0
- data/CHANGELOG.md +5816 -0
- data/Gemfile +30 -0
- data/Gemfile.lock +175 -0
- data/LICENSE.txt +23 -0
- data/Makefile +63 -0
- data/README.md +5655 -0
- data/Rakefile +573 -0
- data/bin/console +38 -0
- data/bin/parse-console +136 -0
- data/bin/server +17 -0
- data/bin/setup +7 -0
- data/config/parse-config.json +12 -0
- data/docs/TEST_SERVER.md +271 -0
- data/docs/_config.yml +1 -0
- data/docs/mcp_guide.md +3484 -0
- data/docs/mongodb_direct_guide.md +1348 -0
- data/docs/mongodb_index_optimization_guide.md +631 -0
- data/examples/transaction_example.rb +219 -0
- data/lib/parse/acl_scope.rb +728 -0
- data/lib/parse/agent/cancellation_token.rb +80 -0
- data/lib/parse/agent/constraint_translator.rb +480 -0
- data/lib/parse/agent/describe.rb +420 -0
- data/lib/parse/agent/errors.rb +133 -0
- data/lib/parse/agent/mcp_client.rb +557 -0
- data/lib/parse/agent/mcp_dispatcher.rb +1023 -0
- data/lib/parse/agent/mcp_rack_app.rb +1143 -0
- data/lib/parse/agent/mcp_server.rb +376 -0
- data/lib/parse/agent/metadata_audit.rb +259 -0
- data/lib/parse/agent/metadata_dsl.rb +733 -0
- data/lib/parse/agent/metadata_registry.rb +794 -0
- data/lib/parse/agent/pipeline_validator.rb +82 -0
- data/lib/parse/agent/prompts.rb +351 -0
- data/lib/parse/agent/rate_limiter.rb +158 -0
- data/lib/parse/agent/relation_graph.rb +162 -0
- data/lib/parse/agent/result_formatter.rb +453 -0
- data/lib/parse/agent/tools.rb +5489 -0
- data/lib/parse/agent.rb +3249 -0
- data/lib/parse/api/aggregate.rb +79 -0
- data/lib/parse/api/all.rb +26 -0
- data/lib/parse/api/analytics.rb +18 -0
- data/lib/parse/api/batch.rb +33 -0
- data/lib/parse/api/cloud_functions.rb +58 -0
- data/lib/parse/api/config.rb +125 -0
- data/lib/parse/api/files.rb +29 -0
- data/lib/parse/api/hooks.rb +117 -0
- data/lib/parse/api/objects.rb +146 -0
- data/lib/parse/api/path_segment.rb +75 -0
- data/lib/parse/api/push.rb +20 -0
- data/lib/parse/api/schema.rb +49 -0
- data/lib/parse/api/server.rb +50 -0
- data/lib/parse/api/sessions.rb +24 -0
- data/lib/parse/api/users.rb +250 -0
- data/lib/parse/atlas_search/index_manager.rb +353 -0
- data/lib/parse/atlas_search/result.rb +204 -0
- data/lib/parse/atlas_search/search_builder.rb +604 -0
- data/lib/parse/atlas_search/session.rb +253 -0
- data/lib/parse/atlas_search.rb +995 -0
- data/lib/parse/client/authentication.rb +97 -0
- data/lib/parse/client/batch.rb +234 -0
- data/lib/parse/client/body_builder.rb +240 -0
- data/lib/parse/client/caching.rb +203 -0
- data/lib/parse/client/logging.rb +293 -0
- data/lib/parse/client/profiling.rb +181 -0
- data/lib/parse/client/protocol.rb +91 -0
- data/lib/parse/client/request.rb +233 -0
- data/lib/parse/client/response.rb +208 -0
- data/lib/parse/client.rb +1104 -0
- data/lib/parse/clp_scope.rb +361 -0
- data/lib/parse/live_query/circuit_breaker.rb +256 -0
- data/lib/parse/live_query/client.rb +1001 -0
- data/lib/parse/live_query/configuration.rb +224 -0
- data/lib/parse/live_query/event.rb +115 -0
- data/lib/parse/live_query/event_queue.rb +272 -0
- data/lib/parse/live_query/health_monitor.rb +214 -0
- data/lib/parse/live_query/logging.rb +149 -0
- data/lib/parse/live_query/subscription.rb +294 -0
- data/lib/parse/live_query.rb +163 -0
- data/lib/parse/lookup_rewriter.rb +445 -0
- data/lib/parse/model/acl.rb +968 -0
- data/lib/parse/model/associations/belongs_to.rb +275 -0
- data/lib/parse/model/associations/collection_proxy.rb +435 -0
- data/lib/parse/model/associations/has_many.rb +597 -0
- data/lib/parse/model/associations/has_one.rb +158 -0
- data/lib/parse/model/associations/pointer_collection_proxy.rb +134 -0
- data/lib/parse/model/associations/relation_collection_proxy.rb +177 -0
- data/lib/parse/model/bytes.rb +62 -0
- data/lib/parse/model/classes/audience.rb +262 -0
- data/lib/parse/model/classes/installation.rb +363 -0
- data/lib/parse/model/classes/job_schedule.rb +153 -0
- data/lib/parse/model/classes/job_status.rb +264 -0
- data/lib/parse/model/classes/product.rb +75 -0
- data/lib/parse/model/classes/push_status.rb +263 -0
- data/lib/parse/model/classes/role.rb +751 -0
- data/lib/parse/model/classes/session.rb +201 -0
- data/lib/parse/model/classes/user.rb +943 -0
- data/lib/parse/model/clp.rb +544 -0
- data/lib/parse/model/core/actions.rb +1268 -0
- data/lib/parse/model/core/builder.rb +139 -0
- data/lib/parse/model/core/create_lock.rb +386 -0
- data/lib/parse/model/core/describe.rb +382 -0
- data/lib/parse/model/core/enhanced_change_tracking.rb +159 -0
- data/lib/parse/model/core/errors.rb +38 -0
- data/lib/parse/model/core/fetching.rb +566 -0
- data/lib/parse/model/core/field_guards.rb +220 -0
- data/lib/parse/model/core/indexing.rb +382 -0
- data/lib/parse/model/core/parse_reference.rb +407 -0
- data/lib/parse/model/core/properties.rb +809 -0
- data/lib/parse/model/core/querying.rb +491 -0
- data/lib/parse/model/core/schema.rb +202 -0
- data/lib/parse/model/core/search_indexing.rb +174 -0
- data/lib/parse/model/date.rb +88 -0
- data/lib/parse/model/email.rb +213 -0
- data/lib/parse/model/file.rb +527 -0
- data/lib/parse/model/geojson.rb +271 -0
- data/lib/parse/model/geopoint.rb +261 -0
- data/lib/parse/model/model.rb +260 -0
- data/lib/parse/model/object.rb +2068 -0
- data/lib/parse/model/phone.rb +520 -0
- data/lib/parse/model/pointer.rb +443 -0
- data/lib/parse/model/polygon.rb +406 -0
- data/lib/parse/model/push.rb +975 -0
- data/lib/parse/model/shortnames.rb +8 -0
- data/lib/parse/model/time_zone.rb +141 -0
- data/lib/parse/model/validations/uniqueness_validator.rb +97 -0
- data/lib/parse/model/validations.rb +96 -0
- data/lib/parse/mongodb.rb +2300 -0
- data/lib/parse/pipeline_security.rb +554 -0
- data/lib/parse/query/constraint.rb +198 -0
- data/lib/parse/query/constraints.rb +3279 -0
- data/lib/parse/query/cursor.rb +434 -0
- data/lib/parse/query/n_plus_one_detector.rb +445 -0
- data/lib/parse/query/operation.rb +104 -0
- data/lib/parse/query/ordering.rb +66 -0
- data/lib/parse/query.rb +7028 -0
- data/lib/parse/schema/index_migrator.rb +291 -0
- data/lib/parse/schema/search_index_migrator.rb +289 -0
- data/lib/parse/schema.rb +494 -0
- data/lib/parse/stack/generators/rails.rb +40 -0
- data/lib/parse/stack/generators/templates/model.erb +51 -0
- data/lib/parse/stack/generators/templates/model_installation.rb +4 -0
- data/lib/parse/stack/generators/templates/model_role.rb +4 -0
- data/lib/parse/stack/generators/templates/model_session.rb +4 -0
- data/lib/parse/stack/generators/templates/model_user.rb +11 -0
- data/lib/parse/stack/generators/templates/parse.rb +12 -0
- data/lib/parse/stack/generators/templates/webhooks.rb +10 -0
- data/lib/parse/stack/railtie.rb +18 -0
- data/lib/parse/stack/tasks.rb +563 -0
- data/lib/parse/stack/version.rb +11 -0
- data/lib/parse/stack.rb +455 -0
- data/lib/parse/two_factor_auth/user_extension.rb +449 -0
- data/lib/parse/two_factor_auth.rb +310 -0
- data/lib/parse/webhooks/payload.rb +360 -0
- data/lib/parse/webhooks/registration.rb +199 -0
- data/lib/parse/webhooks/replay_protection.rb +189 -0
- data/lib/parse/webhooks.rb +510 -0
- data/lib/parse-stack-next.rb +5 -0
- data/lib/parse-stack.rb +5 -0
- data/parse-stack-next.gemspec +82 -0
- data/parse-stack.png +0 -0
- data/scripts/debug-ips.js +35 -0
- data/scripts/docker/Dockerfile.parse +13 -0
- data/scripts/docker/atlas-init.js +284 -0
- data/scripts/docker/docker-compose.atlas.yml +76 -0
- data/scripts/docker/docker-compose.test.yml +106 -0
- data/scripts/docker/mongo-init.js +21 -0
- data/scripts/eval_mcp_with_lm_studio.rb +274 -0
- data/scripts/start-parse.sh +90 -0
- data/scripts/start_mcp_server.rb +78 -0
- data/scripts/test_server_connection.rb +82 -0
- metadata +377 -0
|
@@ -0,0 +1,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
|