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,97 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "faraday"
|
|
5
|
+
require "active_support"
|
|
6
|
+
require "active_support/core_ext"
|
|
7
|
+
|
|
8
|
+
require_relative "protocol"
|
|
9
|
+
|
|
10
|
+
module Parse
|
|
11
|
+
module Middleware
|
|
12
|
+
# This middleware handles sending the proper authentication headers to the
|
|
13
|
+
# Parse REST API endpoint.
|
|
14
|
+
class Authentication < Faraday::Middleware
|
|
15
|
+
include Parse::Protocol
|
|
16
|
+
# @!visibility private
|
|
17
|
+
DISABLE_MASTER_KEY = "X-Disable-Parse-Master-Key".freeze
|
|
18
|
+
# @return [String] the application id for this Parse endpoint.
|
|
19
|
+
attr_accessor :application_id
|
|
20
|
+
# @return [String] the REST API Key for this Parse endpoint.
|
|
21
|
+
attr_accessor :api_key
|
|
22
|
+
# The Master key API Key for this Parse endpoint. This is optional. If
|
|
23
|
+
# provided, it will be sent in every request.
|
|
24
|
+
# @return [String]
|
|
25
|
+
attr_accessor :master_key
|
|
26
|
+
|
|
27
|
+
#
|
|
28
|
+
# @param adapter [Faraday::Adapter] An instance of the Faraday adapter
|
|
29
|
+
# used for the connection. Defaults Faraday::Adapter::NetHttp.
|
|
30
|
+
# @param options [Hash] the options containing Parse authentication data.
|
|
31
|
+
# @option options [String] :application_id the application id.
|
|
32
|
+
# @option options [String] :api_key the REST API key.
|
|
33
|
+
# @option options [String] :master_key the Master Key for this application.
|
|
34
|
+
# If it is set, it will be sent on every request unless this middleware sees
|
|
35
|
+
# {DISABLE_MASTER_KEY} as an entry in the headers section.
|
|
36
|
+
# @option options [String] :content_type the content type format header. Defaults to
|
|
37
|
+
# {Parse::Protocol::CONTENT_TYPE_FORMAT}.
|
|
38
|
+
def initialize(adapter, options = {})
|
|
39
|
+
super(adapter)
|
|
40
|
+
@application_id = options[:application_id]
|
|
41
|
+
@api_key = options[:api_key]
|
|
42
|
+
@master_key = options[:master_key]
|
|
43
|
+
@content_type = options[:content_type] || CONTENT_TYPE_FORMAT
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Thread-safety
|
|
47
|
+
# @!visibility private
|
|
48
|
+
def call(env)
|
|
49
|
+
dup.call!(env)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @!visibility private
|
|
53
|
+
def call!(env)
|
|
54
|
+
# We add the main Parse protocol headers
|
|
55
|
+
headers = {}
|
|
56
|
+
raise ArgumentError, "No Parse Application Id specified for authentication." unless @application_id.present?
|
|
57
|
+
headers[APP_ID] = @application_id
|
|
58
|
+
headers[API_KEY] = @api_key unless @api_key.blank?
|
|
59
|
+
|
|
60
|
+
# Three sources can suppress the master key for this request:
|
|
61
|
+
# 1. The per-request `X-Disable-Parse-Master-Key` header (one-off
|
|
62
|
+
# opt-out, kept for backwards compatibility).
|
|
63
|
+
# 2. Fiber-local state set by {Parse.without_master_key} (block-
|
|
64
|
+
# scoped opt-out, also visible on Faraday retries because the
|
|
65
|
+
# fiber persists across the retry loop — the header at (1)
|
|
66
|
+
# gets stripped on first call and is gone by the retry).
|
|
67
|
+
# 3. A session-token-authenticated request (the existing check
|
|
68
|
+
# below; session token wins over master key).
|
|
69
|
+
header_disable = env[:request_headers][DISABLE_MASTER_KEY].present?
|
|
70
|
+
fiber_disable = Parse.master_key_disabled?
|
|
71
|
+
unless @master_key.blank? || header_disable || fiber_disable
|
|
72
|
+
headers[MASTER_KEY] = @master_key
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
env[:request_headers].delete(DISABLE_MASTER_KEY)
|
|
76
|
+
|
|
77
|
+
# delete the use of master key if we are using session token.
|
|
78
|
+
if env[:request_headers].key?(Parse::Protocol::SESSION_TOKEN)
|
|
79
|
+
headers.delete(MASTER_KEY)
|
|
80
|
+
end
|
|
81
|
+
# merge the headers with the current provided headers
|
|
82
|
+
env[:request_headers].merge! headers
|
|
83
|
+
# set the content type of the request if it was not provided already.
|
|
84
|
+
env[:request_headers][CONTENT_TYPE] ||= @content_type
|
|
85
|
+
# only modify header
|
|
86
|
+
|
|
87
|
+
@app.call(env).on_complete do |response_env|
|
|
88
|
+
# check for return code raise an error when authentication was a failure
|
|
89
|
+
# if response_env[:status] == 401
|
|
90
|
+
# warn "Unauthorized Parse API Credentials for Application Id: #{@application_id}"
|
|
91
|
+
# end
|
|
92
|
+
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end # Authenticator
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "request"
|
|
5
|
+
require_relative "response"
|
|
6
|
+
|
|
7
|
+
module Parse
|
|
8
|
+
# Create a new batch operation.
|
|
9
|
+
# @param reqs [Array<Parse::Request>] a set of requests to batch.
|
|
10
|
+
# @return [BatchOperation] a new {BatchOperation} with the given change requests.
|
|
11
|
+
def self.batch(reqs = nil)
|
|
12
|
+
BatchOperation.new(reqs)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# This class provides a standard way to submit, manage and process batch operations
|
|
16
|
+
# for Parse::Objects and associations.
|
|
17
|
+
#
|
|
18
|
+
# Batch requests are supported implicitly and intelligently through an
|
|
19
|
+
# extension of array. When an array of Parse::Object subclasses is saved,
|
|
20
|
+
# Parse-Stack will batch all possible save operations for the objects in the
|
|
21
|
+
# array that have changed. It will also batch save 50 at a time until all items
|
|
22
|
+
# in the array are saved. Note: Parse does not allow batch saving Parse::User objects.
|
|
23
|
+
#
|
|
24
|
+
# songs = Songs.first 1000 #first 1000 songs
|
|
25
|
+
# songs.each do |song|
|
|
26
|
+
# # ... modify song ...
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# # will batch save 50 items at a time until all are saved.
|
|
30
|
+
# songs.save
|
|
31
|
+
#
|
|
32
|
+
# The objects do not have to be of the same collection in order to be supported in the
|
|
33
|
+
# batch request.
|
|
34
|
+
# @see Array.save
|
|
35
|
+
# @see Array.destroy
|
|
36
|
+
class BatchOperation
|
|
37
|
+
include Enumerable
|
|
38
|
+
|
|
39
|
+
# Default number of threads used to dispatch batch segments concurrently.
|
|
40
|
+
# Raise via `Parse::BatchOperation.parallelism = N` (or pass `parallelism:`
|
|
41
|
+
# to `#submit`) for higher throughput on bulk writes; 2 is intentionally
|
|
42
|
+
# conservative to avoid overwhelming smaller Parse Server deployments.
|
|
43
|
+
DEFAULT_PARALLELISM = 2
|
|
44
|
+
|
|
45
|
+
class << self
|
|
46
|
+
attr_writer :parallelism
|
|
47
|
+
|
|
48
|
+
def parallelism
|
|
49
|
+
@parallelism || DEFAULT_PARALLELISM
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @!attribute requests
|
|
54
|
+
# @return [Array] the set of requests in this batch.
|
|
55
|
+
|
|
56
|
+
# @!attribute responses
|
|
57
|
+
# @return [Array] the set of responses from this batch.
|
|
58
|
+
|
|
59
|
+
# @!attribute transaction
|
|
60
|
+
# @return [Boolean] whether this batch should be executed as a transaction.
|
|
61
|
+
attr_accessor :requests, :responses, :transaction
|
|
62
|
+
|
|
63
|
+
# @return [Parse::Client] the client to be used for the request.
|
|
64
|
+
def client
|
|
65
|
+
@client ||= Parse::Client.client
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @param reqs [Array<Parse::Request>] an array of requests.
|
|
69
|
+
# @param transaction [Boolean] whether to execute as a transaction.
|
|
70
|
+
def initialize(reqs = nil, transaction: false)
|
|
71
|
+
@requests = []
|
|
72
|
+
@responses = []
|
|
73
|
+
@transaction = transaction
|
|
74
|
+
reqs = [reqs] unless reqs.is_a?(Enumerable)
|
|
75
|
+
reqs.each { |r| add(r) } if reqs.is_a?(Enumerable)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Add an additional request to this batch.
|
|
79
|
+
# @overload add(req)
|
|
80
|
+
# @param req [Parse::Request] the request to append.
|
|
81
|
+
# @return [Array<Parse::Request>] the set of requests.
|
|
82
|
+
# @overload add(batch)
|
|
83
|
+
# @param req [Parse::BatchOperation] add all the requests from this batch operation.
|
|
84
|
+
# @return [Array<Parse::Request>] the set of requests.
|
|
85
|
+
def add(req)
|
|
86
|
+
if req.respond_to?(:change_requests)
|
|
87
|
+
requests = req.change_requests.select { |r| r.is_a?(Parse::Request) }
|
|
88
|
+
@requests += requests
|
|
89
|
+
elsif req.is_a?(Array)
|
|
90
|
+
requests = req.select { |r| r.is_a?(Parse::Request) }
|
|
91
|
+
@requests += requests
|
|
92
|
+
elsif req.is_a?(BatchOperation)
|
|
93
|
+
@requests += req.requests if req.is_a?(BatchOperation)
|
|
94
|
+
else
|
|
95
|
+
@requests.push(req) if req.is_a?(Parse::Request)
|
|
96
|
+
end
|
|
97
|
+
@requests
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# This method is for interoperability with Parse::Object instances.
|
|
101
|
+
# @see Parse::Object#change_requests
|
|
102
|
+
def change_requests
|
|
103
|
+
@requests
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# @return [Array]
|
|
107
|
+
def each(&block)
|
|
108
|
+
return enum_for(:each) unless block_given?
|
|
109
|
+
@requests.each(&block)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# @return [Hash] a formatted payload for the batch request.
|
|
113
|
+
def as_json(*args)
|
|
114
|
+
payload = { requests: requests }
|
|
115
|
+
payload[:transaction] = true if @transaction
|
|
116
|
+
payload.as_json
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# @return [Integer] the number of requests in the batch.
|
|
120
|
+
def count
|
|
121
|
+
@requests.count
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Remove all requests in this batch.
|
|
125
|
+
# @return [Array]
|
|
126
|
+
def clear!
|
|
127
|
+
@requests.clear
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# @return [Boolean] true if the request was successful.
|
|
131
|
+
def success?
|
|
132
|
+
return false if @responses.empty?
|
|
133
|
+
@responses.compact.all?(&:success?)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# @return [Boolean] true if the request had an error.
|
|
137
|
+
def error?
|
|
138
|
+
return false if @responses.empty?
|
|
139
|
+
!success?
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Submit the batch operation in chunks until they are all complete. In general,
|
|
143
|
+
# Parse limits requests in each batch to 50 and it is possible that a {BatchOperation}
|
|
144
|
+
# instance contains more than 50 requests. This method will slice up the array of
|
|
145
|
+
# request and send them based on the `segment` amount until they have all been submitted.
|
|
146
|
+
# @param segment [Integer] the number of requests to send in each batch. Default 50.
|
|
147
|
+
# @param parallelism [Integer] the number of segments dispatched in
|
|
148
|
+
# parallel. Defaults to `Parse::BatchOperation.parallelism` (2).
|
|
149
|
+
# @return [Array<Parse::Response>] the corresponding set of responses for
|
|
150
|
+
# each request in the batch.
|
|
151
|
+
def submit(segment = 50, parallelism: self.class.parallelism, &block)
|
|
152
|
+
@responses = []
|
|
153
|
+
@requests.uniq!(&:signature)
|
|
154
|
+
parallelism = 1 if parallelism.nil? || parallelism < 1
|
|
155
|
+
@responses = @requests.each_slice(segment).to_a.threaded_map(parallelism) do |slice|
|
|
156
|
+
client.batch_request(BatchOperation.new(slice))
|
|
157
|
+
end
|
|
158
|
+
@responses.flatten!
|
|
159
|
+
@requests.zip(@responses).each(&block) if block_given?
|
|
160
|
+
@responses
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
alias_method :save, :submit
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
class Array
|
|
168
|
+
|
|
169
|
+
# Submit a batch request for deleting a set of Parse::Objects.
|
|
170
|
+
# @example
|
|
171
|
+
# # assume Post and Author are Parse models
|
|
172
|
+
# author = Author.first
|
|
173
|
+
# posts = Post.all author: author
|
|
174
|
+
# posts.destroy # batch destroy request
|
|
175
|
+
# @return [Parse::BatchOperation] the batch operation performed.
|
|
176
|
+
# @see Parse::BatchOperation
|
|
177
|
+
def destroy
|
|
178
|
+
batch = Parse::BatchOperation.new
|
|
179
|
+
each do |o|
|
|
180
|
+
next unless o.respond_to?(:destroy_request)
|
|
181
|
+
r = o.destroy_request
|
|
182
|
+
batch.add(r) unless r.nil?
|
|
183
|
+
end
|
|
184
|
+
batch.submit
|
|
185
|
+
batch
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Do not alias method as :delete is already part of array.
|
|
189
|
+
# alias_method :delete, :destroy
|
|
190
|
+
|
|
191
|
+
# Submit a batch request for deleting a set of Parse::Objects.
|
|
192
|
+
# Batch requests are supported implicitly and intelligently through an
|
|
193
|
+
# extension of array. When an array of Parse::Object subclasses is saved,
|
|
194
|
+
# Parse-Stack will batch all possible save operations for the objects in the
|
|
195
|
+
# array that have changed. It will also batch save 50 at a time until all items
|
|
196
|
+
# in the array are saved. Note: Parse does not allow batch saving Parse::User objects.
|
|
197
|
+
# @note The objects of the array to be saved do not all have to be of the same collection.
|
|
198
|
+
# @param merge [Boolean] whether to merge the updated changes to the series of
|
|
199
|
+
# objects back to the original ones submitted. If you don't need the original objects
|
|
200
|
+
# to be updated with the changes, set this to false for improved performance.
|
|
201
|
+
# @param force [Boolean] Do not skip objects that do not have pending changes (dirty tracking).
|
|
202
|
+
# @example
|
|
203
|
+
# # assume Post and Author are Parse models
|
|
204
|
+
# author = Author.first
|
|
205
|
+
# posts = Post.first 100
|
|
206
|
+
# posts.each { |post| post.author = author }
|
|
207
|
+
# posts.save # batch save
|
|
208
|
+
# @return [Parse::BatchOperation] the batch operation performed.
|
|
209
|
+
# @see Parse::BatchOperation
|
|
210
|
+
def save(merge: true, force: false)
|
|
211
|
+
batch = Parse::BatchOperation.new
|
|
212
|
+
objects = {}
|
|
213
|
+
each do |o|
|
|
214
|
+
next unless o.is_a?(Parse::Object)
|
|
215
|
+
objects[o.object_id] = o
|
|
216
|
+
batch.add o.change_requests(force)
|
|
217
|
+
end
|
|
218
|
+
if merge == false
|
|
219
|
+
batch.submit
|
|
220
|
+
return batch
|
|
221
|
+
end
|
|
222
|
+
#rebind updates
|
|
223
|
+
batch.submit do |request, response|
|
|
224
|
+
next unless request.tag.present? && response.present? && response.success?
|
|
225
|
+
o = objects[request.tag]
|
|
226
|
+
next unless o.is_a?(Parse::Object)
|
|
227
|
+
result = response.result
|
|
228
|
+
o.id = result["objectId"] if o.id.blank?
|
|
229
|
+
o.set_attributes!(result)
|
|
230
|
+
o.clear_changes!
|
|
231
|
+
end
|
|
232
|
+
batch
|
|
233
|
+
end #save!
|
|
234
|
+
end
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "faraday"
|
|
5
|
+
require_relative "response"
|
|
6
|
+
require_relative "protocol"
|
|
7
|
+
require "active_support"
|
|
8
|
+
require "active_support/core_ext"
|
|
9
|
+
require "active_model/serializers/json"
|
|
10
|
+
require "json"
|
|
11
|
+
require "set"
|
|
12
|
+
|
|
13
|
+
module Parse
|
|
14
|
+
|
|
15
|
+
# @!attribute self.logging
|
|
16
|
+
# Sets {Parse::Middleware::BodyBuilder} logging.
|
|
17
|
+
# You may specify `:debug` for additional verbosity.
|
|
18
|
+
# @return (see Parse::Middleware::BodyBuilder.logging)
|
|
19
|
+
def self.logging
|
|
20
|
+
Parse::Middleware::BodyBuilder.logging
|
|
21
|
+
end
|
|
22
|
+
# @!visibility private
|
|
23
|
+
def self.logging=(value)
|
|
24
|
+
Parse::Middleware::BodyBuilder.logging = value
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Namespace for Parse-Stack related middleware.
|
|
28
|
+
module Middleware
|
|
29
|
+
# This middleware takes an incoming Parse response, after an outgoing request,
|
|
30
|
+
# and creates a Parse::Response object.
|
|
31
|
+
class BodyBuilder < Faraday::Middleware
|
|
32
|
+
include Parse::Protocol
|
|
33
|
+
# Header sent when a GET requests exceeds the limit.
|
|
34
|
+
HTTP_METHOD_OVERRIDE = "X-Http-Method-Override"
|
|
35
|
+
# Maximum url length for most server requests before HTTP Method Override is used.
|
|
36
|
+
MAX_URL_LENGTH = 2_000.freeze
|
|
37
|
+
# Fields that should be redacted from log output.
|
|
38
|
+
SENSITIVE_FIELDS = %w[
|
|
39
|
+
password token sessionToken session_token access_token authData
|
|
40
|
+
masterKey master_key apiKey api_key clientKey client_key
|
|
41
|
+
javascriptKey javascript_key refreshToken refresh_token
|
|
42
|
+
].freeze
|
|
43
|
+
SENSITIVE_PATTERN = /(#{SENSITIVE_FIELDS.join("|")})(["']?\s*[=:>]\s*["']?)([^"&\s,}\]]+)/i
|
|
44
|
+
# Lookup set of sensitive field names for structural (JSON) redaction
|
|
45
|
+
# — case-insensitive match on the key, not the value. Walks the parsed
|
|
46
|
+
# structure so nested objects like {"password":{"nested":"value"}}
|
|
47
|
+
# and escaped-quote payloads (which the regex misses) are scrubbed.
|
|
48
|
+
SENSITIVE_FIELDS_SET = SENSITIVE_FIELDS.map(&:downcase).to_set.freeze
|
|
49
|
+
# Placeholder used in place of redacted values.
|
|
50
|
+
REDACTED_PLACEHOLDER = "[FILTERED]"
|
|
51
|
+
# Request headers that must never be printed verbatim in debug logs.
|
|
52
|
+
# Matched case-insensitively against Faraday header keys.
|
|
53
|
+
REDACTED_HEADERS = [
|
|
54
|
+
Parse::Protocol::MASTER_KEY,
|
|
55
|
+
Parse::Protocol::API_KEY,
|
|
56
|
+
Parse::Protocol::SESSION_TOKEN,
|
|
57
|
+
"X-Parse-JavaScript-Key",
|
|
58
|
+
"Authorization",
|
|
59
|
+
"Cookie",
|
|
60
|
+
].map(&:downcase).freeze
|
|
61
|
+
|
|
62
|
+
class << self
|
|
63
|
+
# Allows logging. Set to `true` to enable logging, `false` to disable.
|
|
64
|
+
# You may specify `:debug` for additional verbosity.
|
|
65
|
+
# @return [Boolean]
|
|
66
|
+
attr_accessor :logging
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Redacts sensitive fields from a string for safe logging.
|
|
70
|
+
#
|
|
71
|
+
# Two passes run in sequence so that no payload shape leaks secrets:
|
|
72
|
+
#
|
|
73
|
+
# 1. **Structural pass.** If the body (after whitespace trim) parses as
|
|
74
|
+
# JSON, the parsed structure is walked recursively. Any value whose
|
|
75
|
+
# key matches +SENSITIVE_FIELDS_SET+ (case-insensitive) is replaced.
|
|
76
|
+
# String values that themselves look like JSON are recursively
|
|
77
|
+
# parsed and scrubbed — catches +{"body":"{\"password\":\"x\"}"}+
|
|
78
|
+
# payloads.
|
|
79
|
+
#
|
|
80
|
+
# 2. **Regex pass.** The result of the structural pass (or the original
|
|
81
|
+
# string if parsing failed) is always also run through the
|
|
82
|
+
# +SENSITIVE_PATTERN+ regex as defense-in-depth. This catches form-
|
|
83
|
+
# encoded bodies, partial JSON, escaped-quote payloads, and string
|
|
84
|
+
# array elements like +["password=hunter2"]+ that the structural
|
|
85
|
+
# walker can't redact in-place.
|
|
86
|
+
# @param str [String] the string to redact.
|
|
87
|
+
# @return [String] the redacted string.
|
|
88
|
+
def self.redact(str)
|
|
89
|
+
s = str.to_s
|
|
90
|
+
return s if s.empty?
|
|
91
|
+
after_structural = s
|
|
92
|
+
if (parsed = try_parse_json(s))
|
|
93
|
+
scrubbed = scrub_sensitive!(parsed)
|
|
94
|
+
begin
|
|
95
|
+
after_structural = scrubbed.to_json
|
|
96
|
+
rescue StandardError
|
|
97
|
+
after_structural = s
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
after_structural.gsub(SENSITIVE_PATTERN) do
|
|
101
|
+
key_part = $1
|
|
102
|
+
sep_part = $2
|
|
103
|
+
val_part = $3
|
|
104
|
+
# Skip values that the structural pass already redacted —
|
|
105
|
+
# otherwise the regex value-class +[^"&\s,}\]]+ stops at the
|
|
106
|
+
# bracket and we end up with +[FILTERED]]+ from the trailing
|
|
107
|
+
# close-bracket left over from +"[FILTERED]"+.
|
|
108
|
+
if val_part == "[FILTERED" || val_part == REDACTED_PLACEHOLDER
|
|
109
|
+
"#{key_part}#{sep_part}#{val_part}"
|
|
110
|
+
else
|
|
111
|
+
"#{key_part}#{sep_part}#{REDACTED_PLACEHOLDER}"
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# @!visibility private
|
|
117
|
+
def self.try_parse_json(str)
|
|
118
|
+
# Find first non-whitespace byte; allow leading whitespace and BOM.
|
|
119
|
+
trimmed = str.byteslice(0, 16).to_s.dup
|
|
120
|
+
trimmed.force_encoding("BINARY")
|
|
121
|
+
trimmed.sub!(/\A\xEF\xBB\xBF/n, "")
|
|
122
|
+
first = trimmed.lstrip[0]
|
|
123
|
+
return nil unless first == "{" || first == "["
|
|
124
|
+
JSON.parse(str, max_nesting: 32)
|
|
125
|
+
rescue JSON::ParserError, JSON::NestingError
|
|
126
|
+
nil
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# @!visibility private
|
|
130
|
+
# Recursively walks a parsed JSON structure replacing values under any
|
|
131
|
+
# sensitive key with the redaction placeholder. Returns the same node
|
|
132
|
+
# for chaining; mutates Hashes/Arrays in place.
|
|
133
|
+
#
|
|
134
|
+
# When a value is itself a String that looks like JSON, attempt to
|
|
135
|
+
# parse-scrub-re-encode it so embedded-JSON payloads are also covered
|
|
136
|
+
# (e.g. +{"body":"{\"password\":\"x\"}"}+).
|
|
137
|
+
def self.scrub_sensitive!(node)
|
|
138
|
+
case node
|
|
139
|
+
when Hash
|
|
140
|
+
node.each do |key, value|
|
|
141
|
+
if key.is_a?(String) && SENSITIVE_FIELDS_SET.include?(key.downcase)
|
|
142
|
+
node[key] = REDACTED_PLACEHOLDER
|
|
143
|
+
elsif value.is_a?(Hash) || value.is_a?(Array)
|
|
144
|
+
scrub_sensitive!(value)
|
|
145
|
+
elsif value.is_a?(String)
|
|
146
|
+
redacted_string = maybe_scrub_embedded_json(value)
|
|
147
|
+
node[key] = redacted_string unless redacted_string.equal?(value)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
when Array
|
|
151
|
+
node.each_with_index do |item, i|
|
|
152
|
+
if item.is_a?(Hash) || item.is_a?(Array)
|
|
153
|
+
scrub_sensitive!(item)
|
|
154
|
+
elsif item.is_a?(String)
|
|
155
|
+
redacted_string = maybe_scrub_embedded_json(item)
|
|
156
|
+
node[i] = redacted_string unless redacted_string.equal?(item)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
node
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# @!visibility private
|
|
164
|
+
# If +str+ parses as JSON (object or array), scrub structurally and
|
|
165
|
+
# re-encode. Otherwise return the original string unchanged.
|
|
166
|
+
def self.maybe_scrub_embedded_json(str)
|
|
167
|
+
return str unless (inner = try_parse_json(str))
|
|
168
|
+
scrub_sensitive!(inner)
|
|
169
|
+
begin
|
|
170
|
+
inner.to_json
|
|
171
|
+
rescue StandardError
|
|
172
|
+
str
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Thread-safety
|
|
177
|
+
# @!visibility private
|
|
178
|
+
def call(env)
|
|
179
|
+
dup.call!(env)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# @!visibility private
|
|
183
|
+
def call!(env)
|
|
184
|
+
# the maximum url size is ~2KB, so if we request a Parse API url greater than this
|
|
185
|
+
# (which is most likely a very complicated query), we need to override the request method
|
|
186
|
+
# to be POST instead of GET and send the query parameters in the body of the POST request.
|
|
187
|
+
# The standard maximum POST request (which is a server setting), is usually set to 20MBs
|
|
188
|
+
if env[:method] == :get && env[:url].to_s.length >= MAX_URL_LENGTH
|
|
189
|
+
env[:request_headers][HTTP_METHOD_OVERRIDE] = "GET"
|
|
190
|
+
env[:request_headers][CONTENT_TYPE] = "application/x-www-form-urlencoded"
|
|
191
|
+
# parse-sever looks for method overrides in the body under the `_method` param.
|
|
192
|
+
# so we will add it to the query string, which will now go into the body.
|
|
193
|
+
env[:body] = "_method=GET&" + env[:url].query
|
|
194
|
+
env[:url].query = nil
|
|
195
|
+
#override
|
|
196
|
+
env[:method] = :post
|
|
197
|
+
# else if not a get, always make sure the request is JSON encoded if the content type matches
|
|
198
|
+
elsif env[:request_headers][CONTENT_TYPE] == CONTENT_TYPE_FORMAT &&
|
|
199
|
+
(env[:body].is_a?(Hash) || env[:body].is_a?(Array))
|
|
200
|
+
env[:body] = env[:body].to_json
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
if self.class.logging
|
|
204
|
+
puts "[Request #{env.method.upcase}] #{self.class.redact(env[:url].to_s)}"
|
|
205
|
+
env[:request_headers].each do |k, v|
|
|
206
|
+
if REDACTED_HEADERS.include?(k.to_s.downcase)
|
|
207
|
+
puts "[Header] #{k} : [FILTERED]"
|
|
208
|
+
else
|
|
209
|
+
puts "[Header] #{k} : #{v}"
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
puts "[Request Body] #{self.class.redact(env[:body].to_s)}"
|
|
214
|
+
end
|
|
215
|
+
@app.call(env).on_complete do |response_env|
|
|
216
|
+
# on a response, create a new Parse::Response and replace the :body
|
|
217
|
+
# of the env
|
|
218
|
+
# @todo CHECK FOR HTTP STATUS CODES
|
|
219
|
+
if self.class.logging
|
|
220
|
+
puts "[[Response #{response_env[:status]}]] ----------------------------------"
|
|
221
|
+
puts self.class.redact(response_env.body.to_s)
|
|
222
|
+
puts "[[Response]] --------------------------------------\n"
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
begin
|
|
226
|
+
r = Parse::Response.new(response_env.body)
|
|
227
|
+
rescue => e
|
|
228
|
+
r = Parse::Response.new
|
|
229
|
+
r.code = response_env.status
|
|
230
|
+
r.error = "Invalid response for #{env[:method]} #{env[:url]}: #{e}"
|
|
231
|
+
end
|
|
232
|
+
r.http_status = response_env[:status]
|
|
233
|
+
r.headers = response_env[:response_headers]
|
|
234
|
+
r.code ||= response_env[:status] if r.error.present?
|
|
235
|
+
response_env[:body] = r
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end #Middleware
|
|
240
|
+
end
|