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
data/lib/parse/stack.rb
ADDED
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
require_relative "stack/version"
|
|
8
|
+
require_relative "client"
|
|
9
|
+
require_relative "query"
|
|
10
|
+
require_relative "model/object"
|
|
11
|
+
require_relative "webhooks"
|
|
12
|
+
require_relative "agent"
|
|
13
|
+
require_relative "two_factor_auth"
|
|
14
|
+
require_relative "two_factor_auth/user_extension"
|
|
15
|
+
require_relative "schema"
|
|
16
|
+
require_relative "schema/index_migrator"
|
|
17
|
+
require_relative "schema/search_index_migrator"
|
|
18
|
+
require_relative "lookup_rewriter"
|
|
19
|
+
|
|
20
|
+
module Parse
|
|
21
|
+
class Error < StandardError; end
|
|
22
|
+
|
|
23
|
+
module Stack
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Fiber-local key consulted by the authentication middleware. A truthy
|
|
27
|
+
# entry suppresses the master-key header for the duration of the block
|
|
28
|
+
# set by {Parse.without_master_key}; a +:enabled+ entry forces the
|
|
29
|
+
# master-key header back on inside a nested {Parse.with_master_key}
|
|
30
|
+
# block.
|
|
31
|
+
MASTER_KEY_STATE_KEY = :__parse_master_key_state__
|
|
32
|
+
|
|
33
|
+
# Run +block+ with the master key suppressed for every Parse request
|
|
34
|
+
# originating in the current fiber. Equivalent to setting the
|
|
35
|
+
# +X-Disable-Parse-Master-Key+ header on each request, but block-scoped
|
|
36
|
+
# so callers can wrap a unit of work — e.g. running an action "as if
|
|
37
|
+
# the configured master key were not available" — without threading
|
|
38
|
+
# the header through every intermediate call.
|
|
39
|
+
#
|
|
40
|
+
# Survives Faraday retries (the per-request header would be stripped on
|
|
41
|
+
# the first attempt and gone by the retry; the fiber-local state lives
|
|
42
|
+
# for the lifetime of the block).
|
|
43
|
+
#
|
|
44
|
+
# @yield runs the block with master-key disabled
|
|
45
|
+
# @return [Object] the block's return value
|
|
46
|
+
# @example
|
|
47
|
+
# Parse.without_master_key do
|
|
48
|
+
# song = Song.find(id) # session-token / API-key auth only
|
|
49
|
+
# song.title = "Renamed"
|
|
50
|
+
# song.save # subject to ACL/CLP
|
|
51
|
+
# end
|
|
52
|
+
def self.without_master_key
|
|
53
|
+
previous = Fiber[MASTER_KEY_STATE_KEY]
|
|
54
|
+
Fiber[MASTER_KEY_STATE_KEY] = :disabled
|
|
55
|
+
yield
|
|
56
|
+
ensure
|
|
57
|
+
Fiber[MASTER_KEY_STATE_KEY] = previous
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Inverse of {.without_master_key}: forces the master key back on for
|
|
61
|
+
# the duration of the block, even if a containing {.without_master_key}
|
|
62
|
+
# had suppressed it. Useful for re-entering an admin-only operation
|
|
63
|
+
# inside a session-scoped block. If no master key is configured on the
|
|
64
|
+
# client, this is a no-op — the helper does not synthesise one.
|
|
65
|
+
#
|
|
66
|
+
# @yield runs the block with master-key enabled (if configured)
|
|
67
|
+
# @return [Object] the block's return value
|
|
68
|
+
def self.with_master_key
|
|
69
|
+
previous = Fiber[MASTER_KEY_STATE_KEY]
|
|
70
|
+
Fiber[MASTER_KEY_STATE_KEY] = :enabled
|
|
71
|
+
yield
|
|
72
|
+
ensure
|
|
73
|
+
Fiber[MASTER_KEY_STATE_KEY] = previous
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# @return [Boolean] true if the current fiber is inside a
|
|
77
|
+
# {.without_master_key} block. Consulted by the authentication
|
|
78
|
+
# middleware in addition to the per-request disable header.
|
|
79
|
+
def self.master_key_disabled?
|
|
80
|
+
Fiber[MASTER_KEY_STATE_KEY] == :disabled
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Configuration for query validation warnings
|
|
84
|
+
# Set to false to disable warnings about unnecessary includes
|
|
85
|
+
# @example Disable query warnings
|
|
86
|
+
# Parse.warn_on_query_issues = false
|
|
87
|
+
@warn_on_query_issues = true
|
|
88
|
+
|
|
89
|
+
# Configuration for debugging autofetch behavior.
|
|
90
|
+
# When set to true, autofetch will raise Parse::AutofetchTriggeredError instead of
|
|
91
|
+
# automatically fetching data. This helps identify where additional keys are needed
|
|
92
|
+
# in queries to avoid unnecessary network requests.
|
|
93
|
+
# @example Enable autofetch debugging
|
|
94
|
+
# Parse.autofetch_raise_on_missing_keys = true
|
|
95
|
+
# # Now accessing an unfetched field will raise an error:
|
|
96
|
+
# # Parse::AutofetchTriggeredError: Autofetch triggered on Post#abc123 - field :content was not fetched
|
|
97
|
+
@autofetch_raise_on_missing_keys = false
|
|
98
|
+
|
|
99
|
+
# Configuration for serialization of partially fetched objects.
|
|
100
|
+
# When set to true (default), calling as_json or to_json on a partially fetched
|
|
101
|
+
# object will only serialize the fields that were fetched, preventing autofetch
|
|
102
|
+
# from being triggered during serialization. This is particularly useful for
|
|
103
|
+
# webhook responses where you intentionally want to return partial data.
|
|
104
|
+
# @example Disable (serialize all fields, triggering autofetch)
|
|
105
|
+
# Parse.serialize_only_fetched_fields = false
|
|
106
|
+
# @example Override per-call
|
|
107
|
+
# user.as_json(only_fetched: false) # Force full serialization
|
|
108
|
+
@serialize_only_fetched_fields = true
|
|
109
|
+
|
|
110
|
+
# Configuration for validating keys in partial fetch operations.
|
|
111
|
+
# When set to true (default), fetch!(keys: [...]) will warn about keys that
|
|
112
|
+
# don't match any defined property on the model. This helps catch typos and
|
|
113
|
+
# undefined field references early.
|
|
114
|
+
# Set to false if you use dynamic schemas or want to suppress warnings.
|
|
115
|
+
# @example Disable key validation warnings
|
|
116
|
+
# Parse.validate_query_keys = false
|
|
117
|
+
# @example With validation enabled (default)
|
|
118
|
+
# song.fetch!(keys: [:title, :nonexistent])
|
|
119
|
+
# # => [Parse::Fetch] Warning: unknown keys [:nonexistent] for Song
|
|
120
|
+
@validate_query_keys = true
|
|
121
|
+
|
|
122
|
+
# Configuration for experimental LiveQuery feature.
|
|
123
|
+
# LiveQuery provides real-time WebSocket subscriptions for reactive applications.
|
|
124
|
+
# This feature is experimental and not fully implemented. Enable at your own risk.
|
|
125
|
+
# @example Enable LiveQuery (experimental)
|
|
126
|
+
# Parse.live_query_enabled = true
|
|
127
|
+
# require 'parse/live_query'
|
|
128
|
+
# @note WebSocket client implementation is incomplete
|
|
129
|
+
@live_query_enabled = false
|
|
130
|
+
|
|
131
|
+
# Configuration for cache write-through on fetch operations.
|
|
132
|
+
# When set to true (default), fetch!/reload!/find operations will:
|
|
133
|
+
# - Skip reading from cache (always get fresh data from server)
|
|
134
|
+
# - Write the fresh data back to cache for future cached reads
|
|
135
|
+
# This is the "write-only" cache mode - ensures data freshness while keeping cache updated.
|
|
136
|
+
# Set to false to completely bypass cache (no read or write) on fetch operations.
|
|
137
|
+
# @example Disable cache write-on-fetch
|
|
138
|
+
# Parse.cache_write_on_fetch = false
|
|
139
|
+
# # Now fetch!/reload!/find will completely bypass cache
|
|
140
|
+
# @example Default behavior (write-only mode)
|
|
141
|
+
# song.fetch! # Gets fresh data, updates cache
|
|
142
|
+
# song.fetch!(cache: true) # Uses cached data if available
|
|
143
|
+
@cache_write_on_fetch = true
|
|
144
|
+
|
|
145
|
+
# Configuration for default query caching behavior.
|
|
146
|
+
# When set to false (default), queries do NOT use cache unless explicitly enabled.
|
|
147
|
+
# When set to true, queries use cache by default (opt-out behavior).
|
|
148
|
+
# This only affects queries - individual queries can always override with cache: true/false.
|
|
149
|
+
# @example Enable cache by default (opt-out behavior)
|
|
150
|
+
# Parse.default_query_cache = true
|
|
151
|
+
# Song.first # Uses cache
|
|
152
|
+
# Song.query(cache: false).first # Explicitly bypasses cache
|
|
153
|
+
# @example Default behavior (opt-in, cache disabled by default)
|
|
154
|
+
# Song.first # Does NOT use cache
|
|
155
|
+
# Song.query(cache: true).first # Explicitly uses cache
|
|
156
|
+
@default_query_cache = false
|
|
157
|
+
|
|
158
|
+
# Configuration for experimental Agent MCP server feature.
|
|
159
|
+
# The MCP (Model Context Protocol) server allows AI agents to interact with Parse data.
|
|
160
|
+
# This feature requires TWO steps to enable for safety:
|
|
161
|
+
# 1. Set environment variable: PARSE_MCP_ENABLED=true
|
|
162
|
+
# 2. Set in code: Parse.mcp_server_enabled = true
|
|
163
|
+
# @example Enable MCP server (experimental)
|
|
164
|
+
# # In environment or .env file:
|
|
165
|
+
# # PARSE_MCP_ENABLED=true
|
|
166
|
+
#
|
|
167
|
+
# # In code:
|
|
168
|
+
# Parse.mcp_server_enabled = true
|
|
169
|
+
# Parse::Agent.enable_mcp!(port: 3001)
|
|
170
|
+
# @note MCP server implementation is experimental
|
|
171
|
+
@mcp_server_enabled = false
|
|
172
|
+
|
|
173
|
+
# Configuration for MCP server port.
|
|
174
|
+
# @example Set custom port
|
|
175
|
+
# Parse.mcp_server_port = 3002
|
|
176
|
+
@mcp_server_port = 3001
|
|
177
|
+
|
|
178
|
+
# Configuration for MCP remote API.
|
|
179
|
+
# When set, the MCP server can forward requests to a remote AI API (e.g., OpenAI, Claude).
|
|
180
|
+
# @example Configure remote API
|
|
181
|
+
# Parse.mcp_remote_api = {
|
|
182
|
+
# provider: :openai, # :openai, :claude, or :custom
|
|
183
|
+
# api_key: ENV['OPENAI_API_KEY'],
|
|
184
|
+
# model: 'gpt-4',
|
|
185
|
+
# base_url: nil # Optional custom base URL
|
|
186
|
+
# }
|
|
187
|
+
@mcp_remote_api = nil
|
|
188
|
+
|
|
189
|
+
# Auto-rewrite LLM-style `$lookup` stages in aggregation pipelines passed
|
|
190
|
+
# to `Parse::Query#aggregate` and `Parse::MongoDB.aggregate`. When true
|
|
191
|
+
# (the default), pipelines using pretty/logical field names (e.g.
|
|
192
|
+
# `localField: "author", foreignField: "_id"`) are translated to the
|
|
193
|
+
# Parse-on-Mongo column-name form (`_p_author`/`parseReference`) when
|
|
194
|
+
# the foreign class declares `parse_reference`. Pipelines already in
|
|
195
|
+
# `_p_*`/`parseReference` form pass through unchanged (idempotent), and
|
|
196
|
+
# when the foreign class lacks `parse_reference` the stage is left
|
|
197
|
+
# alone (no `$split` fallback in the auto path — it's an optimization,
|
|
198
|
+
# not a correction).
|
|
199
|
+
# @example Disable auto-rewrite
|
|
200
|
+
# Parse.rewrite_lookups = false
|
|
201
|
+
@rewrite_lookups = true
|
|
202
|
+
|
|
203
|
+
# Configuration for strict property redefinition checks.
|
|
204
|
+
# When set to true (default), redeclaring a property with a different data type
|
|
205
|
+
# than the existing definition raises ArgumentError instead of warning and
|
|
206
|
+
# silently dropping the new declaration. Identical redeclarations (same data
|
|
207
|
+
# type and remote field name) are always silent. A type mismatch on a core
|
|
208
|
+
# Parse field (e.g. Installation#badge defined as :integer but redeclared as
|
|
209
|
+
# :string) is almost always a bug, so it is a hard failure by default. Set to
|
|
210
|
+
# false to fall back to the legacy warn-and-ignore behavior.
|
|
211
|
+
# @example Opt out of strict redefinition
|
|
212
|
+
# Parse.strict_property_redefinition = false
|
|
213
|
+
@strict_property_redefinition = true
|
|
214
|
+
|
|
215
|
+
# Configuration for globally enabling the synchronize-create lock on
|
|
216
|
+
# `Parse::Object.first_or_create!` and `create_or_update!`. When true, every
|
|
217
|
+
# call to those methods acquires a Moneta-backed mutex (typically Redis) to
|
|
218
|
+
# prevent duplicate creation under concurrency. Per-call `synchronize: false`
|
|
219
|
+
# still opts out. See {Parse::CreateLock}.
|
|
220
|
+
# @example Enable globally
|
|
221
|
+
# Parse.synchronize_create_default = true
|
|
222
|
+
# @example ENV fallback
|
|
223
|
+
# PARSE_STACK_SYNCHRONIZE_CREATE=true
|
|
224
|
+
@synchronize_create_default = ENV["PARSE_STACK_SYNCHRONIZE_CREATE"] == "true"
|
|
225
|
+
|
|
226
|
+
# Configuration for raising on impossible pointer-shape constraints
|
|
227
|
+
# (e.g. bare objectId strings inside an `$in` array against a pointer
|
|
228
|
+
# column whose target class cannot be resolved). When true, the SDK
|
|
229
|
+
# raises {Parse::Query::PointerShapeError} instead of silently
|
|
230
|
+
# returning a value that won't match — preventing the silent-zero
|
|
231
|
+
# failure mode where the LLM/operator reads "0 results" as a real
|
|
232
|
+
# answer. When false (default), the SDK logs a one-shot warning via
|
|
233
|
+
# `Parse.logger` and leaves the value unchanged for backwards
|
|
234
|
+
# compatibility.
|
|
235
|
+
# @example Enable globally
|
|
236
|
+
# Parse.strict_pointer_shapes = true
|
|
237
|
+
# @example ENV fallback (recommended for test/CI)
|
|
238
|
+
# PARSE_STRICT_POINTER_SHAPES=true
|
|
239
|
+
@strict_pointer_shapes = ENV["PARSE_STRICT_POINTER_SHAPES"] == "true"
|
|
240
|
+
|
|
241
|
+
# Tuning bundle for the synchronize-create lock. Per-call kwargs override.
|
|
242
|
+
# Keys: :ttl (seconds, default 3, max 30), :wait (seconds, default 2.0,
|
|
243
|
+
# max 30), :on_degraded (:warn, :warn_throttled, :raise, :proceed).
|
|
244
|
+
# @example
|
|
245
|
+
# Parse.synchronize_create_options = { ttl: 5, wait: 1.0, on_degraded: :warn_throttled }
|
|
246
|
+
@synchronize_create_options = {}
|
|
247
|
+
|
|
248
|
+
# HMAC secret for synchronize-create lock-key derivation. When set, lock
|
|
249
|
+
# keys are HMAC-SHA256 of the canonical payload (hides query_attrs content
|
|
250
|
+
# from Redis MONITOR / snapshot snoopers). When unset and the cache store
|
|
251
|
+
# is Redis-backed, a one-time warning is emitted and plain SHA256 is used
|
|
252
|
+
# so cross-process locking still works. When unset and the store is the
|
|
253
|
+
# in-memory adapter, an auto-derived per-process secret is used.
|
|
254
|
+
# @example
|
|
255
|
+
# Parse.synchronize_create_secret = ENV["PARSE_STACK_LOCK_SECRET"]
|
|
256
|
+
@synchronize_create_secret = nil
|
|
257
|
+
|
|
258
|
+
# Optional dedicated Moneta store for the synchronize-create lock. When
|
|
259
|
+
# nil, falls back to {Parse.cache}.
|
|
260
|
+
# @example
|
|
261
|
+
# Parse.synchronize_create_store = Moneta.new(:Redis, url: "redis://locks:6379/1")
|
|
262
|
+
@synchronize_create_store = nil
|
|
263
|
+
|
|
264
|
+
# Optional allowlist of {Parse::Object} subclasses that may use the
|
|
265
|
+
# synchronize-create lock. When set, calls from any other class raise
|
|
266
|
+
# {Parse::CreateLockUnavailableError}. When nil (default) with the global
|
|
267
|
+
# default enabled, a one-time +[Parse::Stack:SECURITY]+ warning is emitted
|
|
268
|
+
# noting the unbounded surface; the lock still applies to every class.
|
|
269
|
+
#
|
|
270
|
+
# **Inheritance behavior:** The allowlist check in
|
|
271
|
+
# {Parse::Core::Actions::ClassMethods#_assert_synchronize_class_allowed!}
|
|
272
|
+
# uses `self <= entry`, so any subclass of an allowlisted Class entry is
|
|
273
|
+
# itself allowlisted. Allowlisting `User` transitively allowlists every
|
|
274
|
+
# `class GuestUser < User` / `class AdminUser < User` etc. — declared now
|
|
275
|
+
# OR ever defined later in the process. If you need strict per-class
|
|
276
|
+
# gating, pass entries as String names (`"User"`) — those are matched
|
|
277
|
+
# against `self.name` / `parse_class` only, with no inheritance walk.
|
|
278
|
+
# @example Restrict to specific classes (subclasses inherit)
|
|
279
|
+
# Parse.synchronize_classes = [User, Device, Subscription]
|
|
280
|
+
# @example Strict equality, no inheritance
|
|
281
|
+
# Parse.synchronize_classes = ["User", "Device", "Subscription"]
|
|
282
|
+
@synchronize_classes = nil
|
|
283
|
+
|
|
284
|
+
class << self
|
|
285
|
+
attr_accessor :warn_on_query_issues, :autofetch_raise_on_missing_keys, :serialize_only_fetched_fields, :validate_query_keys,
|
|
286
|
+
:live_query_enabled, :cache_write_on_fetch, :default_query_cache, :mcp_server_enabled, :mcp_server_port, :mcp_remote_api,
|
|
287
|
+
:rewrite_lookups, :strict_property_redefinition,
|
|
288
|
+
:synchronize_create_default, :synchronize_create_options, :synchronize_create_secret,
|
|
289
|
+
:synchronize_create_store, :synchronize_classes,
|
|
290
|
+
:strict_pointer_shapes
|
|
291
|
+
|
|
292
|
+
# Check if LiveQuery feature is enabled
|
|
293
|
+
# @return [Boolean]
|
|
294
|
+
def live_query_enabled?
|
|
295
|
+
@live_query_enabled == true
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# Check if strict pointer-shape validation is enabled. When true,
|
|
299
|
+
# impossible shapes (e.g. bare string `$in` element against a
|
|
300
|
+
# pointer column whose target class is unknown) raise
|
|
301
|
+
# {Parse::Query::PointerShapeError} instead of silently returning
|
|
302
|
+
# zero rows. See {Parse.strict_pointer_shapes=}.
|
|
303
|
+
# @return [Boolean]
|
|
304
|
+
def strict_pointer_shapes?
|
|
305
|
+
@strict_pointer_shapes == true
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Check if MCP server feature is enabled
|
|
309
|
+
# Requires PARSE_MCP_ENABLED=true in environment AND Parse.mcp_server_enabled = true
|
|
310
|
+
# @return [Boolean]
|
|
311
|
+
def mcp_server_enabled?
|
|
312
|
+
return false unless ENV["PARSE_MCP_ENABLED"] == "true"
|
|
313
|
+
@mcp_server_enabled == true
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Configure MCP remote API connection
|
|
317
|
+
# @param provider [Symbol] the API provider (:openai, :claude, :custom)
|
|
318
|
+
# @param api_key [String] the API key
|
|
319
|
+
# @param model [String] the model to use (e.g., 'gpt-4', 'claude-3-opus')
|
|
320
|
+
# @param base_url [String, nil] optional custom base URL
|
|
321
|
+
# @return [Hash] the configuration hash
|
|
322
|
+
def configure_mcp_remote_api(provider:, api_key:, model: nil, base_url: nil)
|
|
323
|
+
@mcp_remote_api = {
|
|
324
|
+
provider: provider.to_sym,
|
|
325
|
+
api_key: api_key,
|
|
326
|
+
model: model,
|
|
327
|
+
base_url: base_url,
|
|
328
|
+
}
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Check if MCP remote API is configured
|
|
332
|
+
# @return [Boolean]
|
|
333
|
+
def mcp_remote_api_configured?
|
|
334
|
+
@mcp_remote_api.is_a?(Hash) && @mcp_remote_api[:api_key].present?
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# Error raised when {Parse::CreateLock#synchronize} cannot acquire the
|
|
339
|
+
# mutex within the configured wait budget. Callers typically rescue and either
|
|
340
|
+
# retry or treat as a temporary unavailability.
|
|
341
|
+
class CreateLockTimeoutError < Parse::Error; end
|
|
342
|
+
|
|
343
|
+
# Error raised when query_attrs passed to a synchronized `first_or_create!`
|
|
344
|
+
# contain values that cannot be canonicalized into a stable lock key (Procs,
|
|
345
|
+
# Regexps, query operators, unsaved pointers, nested Hashes, oversized
|
|
346
|
+
# payloads).
|
|
347
|
+
class CreateLockInvalidKey < Parse::Error; end
|
|
348
|
+
|
|
349
|
+
# Error raised when a synchronized call is made but the lock store is
|
|
350
|
+
# unavailable (typically `on_degraded: :raise` was configured and the store
|
|
351
|
+
# is process-local).
|
|
352
|
+
class CreateLockUnavailableError < Parse::Error; end
|
|
353
|
+
|
|
354
|
+
# Error raised when autofetch would be triggered but Parse.autofetch_raise_on_missing_keys is true.
|
|
355
|
+
# This helps developers identify where they need to add additional keys to their queries.
|
|
356
|
+
class AutofetchTriggeredError < StandardError
|
|
357
|
+
attr_reader :klass, :parse_object_id, :field, :is_pointer
|
|
358
|
+
|
|
359
|
+
def initialize(klass, object_id, field, is_pointer:)
|
|
360
|
+
@klass = klass
|
|
361
|
+
@parse_object_id = object_id
|
|
362
|
+
@field = field
|
|
363
|
+
@is_pointer = is_pointer
|
|
364
|
+
|
|
365
|
+
if is_pointer
|
|
366
|
+
super("Autofetch triggered on #{klass}##{object_id} - pointer accessed field :#{field}. Add this field to your includes or fetch the object first.")
|
|
367
|
+
else
|
|
368
|
+
super("Autofetch triggered on #{klass}##{object_id} - field :#{field} was not included in partial fetch. Add :#{field} to your query keys.")
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# Special class to support Modernistik Hyperdrive server.
|
|
374
|
+
class Hyperdrive
|
|
375
|
+
# Applies a remote JSON hash containing the ENV keys and values from a remote
|
|
376
|
+
# URL. Values from the JSON hash are only applied to the current ENV hash ONLY if
|
|
377
|
+
# it does not already have a value. Therefore local ENV values will take precedence
|
|
378
|
+
# over remote ones. By default, it uses the url in environment value in 'CONFIG_URL' or 'HYPERDRIVE_URL'.
|
|
379
|
+
# @param url [String] the remote url that responds with the JSON body.
|
|
380
|
+
# @return [Boolean] true if the JSON hash was found and applied successfully.
|
|
381
|
+
def self.config!(url = nil)
|
|
382
|
+
url ||= ENV["HYPERDRIVE_URL"] || ENV["CONFIG_URL"]
|
|
383
|
+
return false if url.blank?
|
|
384
|
+
|
|
385
|
+
begin
|
|
386
|
+
uri = URI.parse(url)
|
|
387
|
+
|
|
388
|
+
# Security: Only allow HTTPS or localhost HTTP for development
|
|
389
|
+
unless uri.is_a?(URI::HTTPS) || (uri.is_a?(URI::HTTP) && %w[localhost 127.0.0.1].include?(uri.host))
|
|
390
|
+
warn "[Parse::Stack] Security: Config URL must be HTTPS (got: #{url})"
|
|
391
|
+
return false
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
# Use Net::HTTP instead of open-uri to avoid command injection via pipe characters
|
|
395
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
396
|
+
http.use_ssl = uri.scheme == "https"
|
|
397
|
+
http.open_timeout = 10
|
|
398
|
+
http.read_timeout = 10
|
|
399
|
+
|
|
400
|
+
request = Net::HTTP::Get.new(uri.request_uri)
|
|
401
|
+
response = http.request(request)
|
|
402
|
+
|
|
403
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
404
|
+
warn "[Parse::Stack] Config fetch failed: #{url} (HTTP #{response.code})"
|
|
405
|
+
return false
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
# Parse JSON safely
|
|
409
|
+
remote_config = JSON.parse(response.body)
|
|
410
|
+
|
|
411
|
+
unless remote_config.is_a?(Hash)
|
|
412
|
+
warn "[Parse::Stack] Config must be a JSON object: #{url}"
|
|
413
|
+
return false
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
remote_config.each do |key, value|
|
|
417
|
+
k = key.to_s.upcase
|
|
418
|
+
# Validate key format to prevent injection
|
|
419
|
+
next unless k.match?(/\A[A-Z][A-Z0-9_]*\z/)
|
|
420
|
+
next unless ENV[k].nil?
|
|
421
|
+
ENV[k] = value.to_s
|
|
422
|
+
end
|
|
423
|
+
true
|
|
424
|
+
rescue URI::InvalidURIError => e
|
|
425
|
+
warn "[Parse::Stack] Invalid config URL: #{url} (#{e.message})"
|
|
426
|
+
false
|
|
427
|
+
rescue JSON::ParserError => e
|
|
428
|
+
warn "[Parse::Stack] Invalid JSON in config: #{url} (#{e.message})"
|
|
429
|
+
false
|
|
430
|
+
rescue StandardError => e
|
|
431
|
+
warn "[Parse::Stack] Error loading config: #{url} (#{e.class}: #{e.message})"
|
|
432
|
+
false
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
# Startup warning: If ENV is set but programmatic flag isn't, warn the user
|
|
439
|
+
if ENV["PARSE_MCP_ENABLED"] == "true" && !Parse.instance_variable_get(:@mcp_server_enabled)
|
|
440
|
+
warn "[Parse::Stack] PARSE_MCP_ENABLED is set in environment but Parse.mcp_server_enabled is false. " \
|
|
441
|
+
"Call Parse.mcp_server_enabled = true to enable the MCP agent feature."
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
# Startup warning: synchronize-create global-default mode without a class
|
|
445
|
+
# allowlist exposes the whole first_or_create!/create_or_update! surface to
|
|
446
|
+
# attacker-controlled lock contention. Operators should either restrict via
|
|
447
|
+
# Parse.synchronize_classes or audit each call site that takes untrusted input.
|
|
448
|
+
if Parse.synchronize_create_default && Parse.synchronize_classes.nil?
|
|
449
|
+
warn "[Parse::Stack:SECURITY] Parse.synchronize_create_default is true with no Parse.synchronize_classes allowlist. " \
|
|
450
|
+
"Every first_or_create!/create_or_update! caller is now subject to Redis-backed lock contention; an attacker " \
|
|
451
|
+
"controlling query_attrs on a public path can hold lock keys × TTL. Set Parse.synchronize_classes = [User, …] " \
|
|
452
|
+
"to restrict the surface, or audit each call site."
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
require_relative "stack/railtie" if defined?(::Rails)
|