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,382 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Parse
|
|
5
|
+
module Core
|
|
6
|
+
# Operator-facing introspection mixin. Extended onto Parse::Object so
|
|
7
|
+
# `Model.describe` aggregates local model declarations, server schema,
|
|
8
|
+
# CLP, and Atlas Search index state into a single Hash.
|
|
9
|
+
#
|
|
10
|
+
# SECURITY POSTURE — mirrors {Parse::Agent::Describe}. This is
|
|
11
|
+
# operator-side observability, NOT data exposed to an LLM. Output is
|
|
12
|
+
# never included in tool responses, MCP `tools/list`, or any
|
|
13
|
+
# `parse.agent.*` notification payload. Surfacing it via a console or
|
|
14
|
+
# debug endpoint requires auth-gating on the operator boundary.
|
|
15
|
+
#
|
|
16
|
+
# Network policy mirrors `agent.describe`: local-only by default. Opt
|
|
17
|
+
# in to server fetches with `network: true`. Each section degrades
|
|
18
|
+
# gracefully (`{available: false, reason: ...}`) instead of raising
|
|
19
|
+
# when the underlying service is unreachable or unconfigured.
|
|
20
|
+
module Describe
|
|
21
|
+
LOCAL_SECTIONS = %i[model acl].freeze
|
|
22
|
+
NETWORK_SECTIONS = %i[schema clp atlas indexes].freeze
|
|
23
|
+
ALL_SECTIONS = (LOCAL_SECTIONS + NETWORK_SECTIONS).freeze
|
|
24
|
+
|
|
25
|
+
# Core/built-in field keys we don't report under `:model[:fields]` —
|
|
26
|
+
# they're inherited from Parse::Object (in both snake_case and
|
|
27
|
+
# camelCase form) and add noise to every output.
|
|
28
|
+
CORE_FIELD_KEYS = %i[
|
|
29
|
+
id object_id created_at updated_at acl session_token
|
|
30
|
+
objectId createdAt updatedAt ACL sessionToken
|
|
31
|
+
].freeze
|
|
32
|
+
|
|
33
|
+
# Aggregate introspection for the class. Local-only by default; pass
|
|
34
|
+
# `network: true` to include server schema, CLP, and Atlas Search.
|
|
35
|
+
#
|
|
36
|
+
# @param sections [Array<Symbol>] which sections to include. When
|
|
37
|
+
# empty, returns LOCAL_SECTIONS for `network: false` and
|
|
38
|
+
# ALL_SECTIONS for `network: true`. Valid: :model :acl :schema
|
|
39
|
+
# :clp :atlas.
|
|
40
|
+
# @param pretty [Boolean] when true, returns a multi-line String for
|
|
41
|
+
# `puts` debugging instead of the Hash.
|
|
42
|
+
# @param network [Boolean] permit per-section server/Mongo fetches.
|
|
43
|
+
# When false, network sections short-circuit to
|
|
44
|
+
# `{available: false, reason: :network_disabled}`.
|
|
45
|
+
# @param client [Parse::Client, nil] optional client override for
|
|
46
|
+
# schema/clp fetches.
|
|
47
|
+
# @param master [Boolean] forward an explicit master-key opt-in to
|
|
48
|
+
# admin-only sub-fetches (currently: `$indexStats` via
|
|
49
|
+
# {Parse::MongoDB.index_stats}, which requires `master: true`).
|
|
50
|
+
# When false (default), `usage:` counters degrade to `{}` and the
|
|
51
|
+
# `indexes` section reports `usage_available: false`. Pass
|
|
52
|
+
# `master: true` from an operator/audit context to populate real
|
|
53
|
+
# counters. The flag is NEVER auto-set by the SDK.
|
|
54
|
+
# @note Valid sections: :model :acl :schema :clp :atlas :indexes.
|
|
55
|
+
# @return [Hash, String]
|
|
56
|
+
def describe(*sections, pretty: false, network: false, usage: false, master: false, client: nil)
|
|
57
|
+
requested = sections.flatten.map(&:to_sym)
|
|
58
|
+
active = if requested.empty?
|
|
59
|
+
network ? ALL_SECTIONS : LOCAL_SECTIONS
|
|
60
|
+
else
|
|
61
|
+
requested
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
data = { class_name: parse_class }
|
|
65
|
+
active.each do |s|
|
|
66
|
+
data[s] = describe_section(s, client: client, network: network, usage: usage, master: master)
|
|
67
|
+
end
|
|
68
|
+
pretty ? describe_pretty(data) : data
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def describe_section(section, client:, network:, usage: false, master: false)
|
|
74
|
+
case section
|
|
75
|
+
when :model then describe_model_section
|
|
76
|
+
when :acl then describe_acl_section
|
|
77
|
+
when :schema then network_section(network) { describe_schema_section(client) }
|
|
78
|
+
when :clp then network_section(network) { describe_clp_section(client) }
|
|
79
|
+
when :atlas then network_section(network) { describe_atlas_section }
|
|
80
|
+
when :indexes then network_section(network) { describe_indexes_section(usage: usage, master: master) }
|
|
81
|
+
else { available: false, reason: :unknown_section }
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def network_section(network)
|
|
86
|
+
return { available: false, reason: :network_disabled } unless network
|
|
87
|
+
yield
|
|
88
|
+
rescue StandardError => e
|
|
89
|
+
{ available: false, reason: :error, error: e.class.name, message: e.message }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def describe_model_section
|
|
93
|
+
local_fields = fields.reject { |k, _| CORE_FIELD_KEYS.include?(k) }
|
|
94
|
+
{
|
|
95
|
+
parse_class: parse_class,
|
|
96
|
+
fields: local_fields,
|
|
97
|
+
field_count: local_fields.size,
|
|
98
|
+
references: (respond_to?(:references) ? references.dup : {}),
|
|
99
|
+
relations: (respond_to?(:relations) ? relations.dup : {}),
|
|
100
|
+
defaults: (respond_to?(:defaults_list) ? defaults_list.dup : []),
|
|
101
|
+
enums: (respond_to?(:enums) ? enums.dup : {}),
|
|
102
|
+
agent_fields: (respond_to?(:agent_field_allowlist) && agent_field_allowlist.any? ?
|
|
103
|
+
agent_field_allowlist.map(&:to_s).sort : nil),
|
|
104
|
+
agent_methods: agent_method_names_or_nil,
|
|
105
|
+
}
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def agent_method_names_or_nil
|
|
109
|
+
return nil unless respond_to?(:agent_methods)
|
|
110
|
+
methods = agent_methods
|
|
111
|
+
return nil if methods.nil? || methods.empty?
|
|
112
|
+
methods.keys.map(&:to_s).sort
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def describe_acl_section
|
|
116
|
+
defaults = respond_to?(:default_acls) ? default_acls : nil
|
|
117
|
+
{
|
|
118
|
+
default_acl: defaults.respond_to?(:as_json) ? defaults.as_json : nil,
|
|
119
|
+
default_acl_private: (respond_to?(:default_acl_private) ? !!default_acl_private : nil),
|
|
120
|
+
acl_policy: instance_variable_get(:@acl_policy_setting),
|
|
121
|
+
}
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def describe_schema_section(client)
|
|
125
|
+
info = Parse::Schema.fetch(parse_class, client: client)
|
|
126
|
+
return { available: false, reason: :class_missing_on_server } if info.nil?
|
|
127
|
+
diff = Parse::Schema::SchemaDiff.new(self, info)
|
|
128
|
+
{
|
|
129
|
+
available: true,
|
|
130
|
+
in_sync: diff.in_sync?,
|
|
131
|
+
server_field_count: info.field_names.size,
|
|
132
|
+
missing_on_server: diff.missing_on_server,
|
|
133
|
+
missing_locally: diff.missing_locally,
|
|
134
|
+
type_mismatches: diff.type_mismatches,
|
|
135
|
+
}
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def describe_clp_section(client)
|
|
139
|
+
info = Parse::Schema.fetch(parse_class, client: client)
|
|
140
|
+
return { available: false, reason: :class_missing_on_server } if info.nil?
|
|
141
|
+
{ available: true, class_level_permissions: info.class_level_permissions }
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def describe_atlas_section
|
|
145
|
+
unless defined?(Parse::MongoDB) && Parse::MongoDB.respond_to?(:enabled?) && Parse::MongoDB.enabled?
|
|
146
|
+
return { available: false, reason: :mongodb_not_enabled }
|
|
147
|
+
end
|
|
148
|
+
indexes = Parse::AtlasSearch::IndexManager.list_indexes(parse_class)
|
|
149
|
+
{
|
|
150
|
+
available: true,
|
|
151
|
+
count: indexes.size,
|
|
152
|
+
indexes: indexes.map { |i|
|
|
153
|
+
{ name: i["name"],
|
|
154
|
+
status: i["status"],
|
|
155
|
+
queryable: i["queryable"] }
|
|
156
|
+
},
|
|
157
|
+
}
|
|
158
|
+
rescue => e
|
|
159
|
+
msg = e.message.to_s
|
|
160
|
+
reason = if defined?(Parse::AtlasSearch::NotAvailable) && e.is_a?(Parse::AtlasSearch::NotAvailable)
|
|
161
|
+
:atlas_not_available
|
|
162
|
+
else
|
|
163
|
+
:error
|
|
164
|
+
end
|
|
165
|
+
{ available: false, reason: reason, error: e.class.name, message: msg }
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def describe_indexes_section(usage: false, master: false)
|
|
169
|
+
unless defined?(Parse::MongoDB) && Parse::MongoDB.respond_to?(:enabled?) && Parse::MongoDB.enabled?
|
|
170
|
+
return { available: false, reason: :mongodb_not_enabled }
|
|
171
|
+
end
|
|
172
|
+
raw = Parse::MongoDB.indexes(parse_class)
|
|
173
|
+
# `Parse::MongoDB.index_stats` requires explicit `master: true`
|
|
174
|
+
# because `$indexStats` discloses cluster metadata; without the
|
|
175
|
+
# opt-in it rescues to `{}`, which surfaces as
|
|
176
|
+
# `usage_available: false` below. Forward the caller's `master:`
|
|
177
|
+
# so operator/audit callers can populate real counters.
|
|
178
|
+
stats = if usage
|
|
179
|
+
master ? Parse::MongoDB.index_stats(parse_class, master: true) : {}
|
|
180
|
+
else
|
|
181
|
+
{}
|
|
182
|
+
end
|
|
183
|
+
normalized = raw.map { |idx| normalize_index_entry(idx, stats: stats) }
|
|
184
|
+
result = {
|
|
185
|
+
available: true,
|
|
186
|
+
count: normalized.size,
|
|
187
|
+
indexes: normalized,
|
|
188
|
+
}
|
|
189
|
+
if usage
|
|
190
|
+
# Empty stats Hash means the role lacks clusterMonitor / Atlas
|
|
191
|
+
# restricts $indexStats; surface that so the operator can act
|
|
192
|
+
# on it rather than reading absent counters as "zero traffic".
|
|
193
|
+
result[:usage_available] = !stats.empty?
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
if respond_to?(:mongo_index_declarations) && mongo_index_declarations.any?
|
|
197
|
+
# Migrator surfaces declared/drift only when the class opted
|
|
198
|
+
# into the DSL — keeps the describe output clean for classes
|
|
199
|
+
# that have not adopted `mongo_index`. Plan is a Hash keyed by
|
|
200
|
+
# collection — surface the parent's parse_class plan as
|
|
201
|
+
# `declared/drift/capacity` for the simple single-collection
|
|
202
|
+
# case, and add a `relations:` sub-key listing per-collection
|
|
203
|
+
# plans for any `_Join:*` collections from
|
|
204
|
+
# `mongo_relation_index`.
|
|
205
|
+
plans = Parse::Schema::IndexMigrator.new(self).plan
|
|
206
|
+
base = parse_class
|
|
207
|
+
parent_plan = plans[base]
|
|
208
|
+
if parent_plan
|
|
209
|
+
result[:declared] = parent_plan[:declared].map { |d| describe_decl(d) }
|
|
210
|
+
result[:drift] = describe_drift(parent_plan)
|
|
211
|
+
result[:parse_managed] = parent_plan[:parse_managed]
|
|
212
|
+
result[:capacity] = describe_capacity(parent_plan)
|
|
213
|
+
end
|
|
214
|
+
join_plans = plans.reject { |k, _| k == base }
|
|
215
|
+
unless join_plans.empty?
|
|
216
|
+
result[:relations] = join_plans.each_with_object({}) do |(coll, p), h|
|
|
217
|
+
h[coll] = {
|
|
218
|
+
declared: p[:declared].map { |d| describe_decl(d) },
|
|
219
|
+
drift: describe_drift(p),
|
|
220
|
+
parse_managed: p[:parse_managed],
|
|
221
|
+
capacity: describe_capacity(p),
|
|
222
|
+
}
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
result
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def describe_decl(decl)
|
|
231
|
+
{ keys: decl[:keys], options: decl[:options], collection: decl[:collection] }
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def describe_drift(plan)
|
|
235
|
+
{
|
|
236
|
+
to_create: plan[:to_create].map { |d| describe_decl(d) },
|
|
237
|
+
in_sync: plan[:in_sync].map { |d| describe_decl(d) },
|
|
238
|
+
orphans: plan[:orphans],
|
|
239
|
+
conflicts: plan[:conflicts],
|
|
240
|
+
}
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def describe_capacity(plan)
|
|
244
|
+
{
|
|
245
|
+
used: plan[:capacity_used],
|
|
246
|
+
after: plan[:capacity_after],
|
|
247
|
+
remaining: plan[:capacity_remaining],
|
|
248
|
+
ok: plan[:capacity_ok],
|
|
249
|
+
}
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Pull out the operator-relevant fields and coerce BSON values into
|
|
253
|
+
# JSON-safe primitives so the hash can be `JSON.dump`'d without
|
|
254
|
+
# surprises. The driver returns BSON::ObjectId / BSON::Regexp::Raw
|
|
255
|
+
# inside `partialFilterExpression` for some index shapes.
|
|
256
|
+
#
|
|
257
|
+
# When `stats:` is supplied (from `$indexStats`), merges in the
|
|
258
|
+
# `usage:` sub-hash with `ops` and `since` counters for the index.
|
|
259
|
+
def normalize_index_entry(idx, stats: {})
|
|
260
|
+
name = idx["name"] || idx[:name]
|
|
261
|
+
entry = {
|
|
262
|
+
name: name,
|
|
263
|
+
implicit_id: name == "_id_",
|
|
264
|
+
key: coerce_bson(idx["key"] || idx[:key] || {}),
|
|
265
|
+
unique: idx["unique"] == true,
|
|
266
|
+
sparse: idx["sparse"] == true,
|
|
267
|
+
partial_filter: coerce_bson(idx["partialFilterExpression"] || idx[:partialFilterExpression]),
|
|
268
|
+
expire_after_seconds: idx["expireAfterSeconds"] || idx[:expireAfterSeconds],
|
|
269
|
+
}
|
|
270
|
+
if (stat = stats[name])
|
|
271
|
+
entry[:usage] = { ops: stat[:ops], since: stat[:since] }
|
|
272
|
+
end
|
|
273
|
+
entry
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def coerce_bson(value)
|
|
277
|
+
case value
|
|
278
|
+
when Hash
|
|
279
|
+
value.each_with_object({}) { |(k, v), h| h[k.to_s] = coerce_bson(v) }
|
|
280
|
+
when Array
|
|
281
|
+
value.map { |v| coerce_bson(v) }
|
|
282
|
+
when Symbol, String, Numeric, TrueClass, FalseClass, NilClass
|
|
283
|
+
value
|
|
284
|
+
else
|
|
285
|
+
value.to_s
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def describe_pretty(data)
|
|
290
|
+
lines = ["#{data[:class_name]} describe:"]
|
|
291
|
+
|
|
292
|
+
if (m = data[:model])
|
|
293
|
+
lines << " fields: #{m[:field_count]}"
|
|
294
|
+
lines << " references: #{m[:references].inspect}" if m[:references].any?
|
|
295
|
+
lines << " relations: #{m[:relations].inspect}" if m[:relations].any?
|
|
296
|
+
lines << " defaults: #{m[:defaults].inspect}" if m[:defaults].any?
|
|
297
|
+
lines << " enums: #{m[:enums].keys.inspect}" if m[:enums].any?
|
|
298
|
+
lines << " agent_fields: #{m[:agent_fields].inspect}" if m[:agent_fields]
|
|
299
|
+
lines << " agent_methods: #{m[:agent_methods].inspect}" if m[:agent_methods]
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
if (a = data[:acl])
|
|
303
|
+
lines << " default_acl: #{a[:default_acl].inspect}"
|
|
304
|
+
lines << " acl_policy: #{a[:acl_policy].inspect}" if a[:acl_policy]
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
if (s = data[:schema])
|
|
308
|
+
if s[:available]
|
|
309
|
+
label = s[:in_sync] ? "in sync" : "drifted"
|
|
310
|
+
lines << " schema: #{label} (server fields=#{s[:server_field_count]})"
|
|
311
|
+
lines << " missing_on_server: #{s[:missing_on_server].keys.inspect}" if s[:missing_on_server].any?
|
|
312
|
+
lines << " missing_locally: #{s[:missing_locally].keys.inspect}" if s[:missing_locally].any?
|
|
313
|
+
lines << " type_mismatches: #{s[:type_mismatches].keys.inspect}" if s[:type_mismatches].any?
|
|
314
|
+
else
|
|
315
|
+
lines << " schema: unavailable (#{s[:reason]})"
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
if (c = data[:clp])
|
|
320
|
+
if c[:available]
|
|
321
|
+
lines << " clp: #{c[:class_level_permissions].keys.inspect}"
|
|
322
|
+
else
|
|
323
|
+
lines << " clp: unavailable (#{c[:reason]})"
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
if (x = data[:atlas])
|
|
328
|
+
if x[:available]
|
|
329
|
+
lines << " atlas_search: #{x[:count]} index(es)"
|
|
330
|
+
x[:indexes].each { |i| lines << " - #{i[:name]} (#{i[:status]}, queryable=#{i[:queryable]})" }
|
|
331
|
+
else
|
|
332
|
+
lines << " atlas_search: unavailable (#{x[:reason]})"
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
if (ix = data[:indexes])
|
|
337
|
+
if ix[:available]
|
|
338
|
+
lines << " indexes: #{ix[:count]}"
|
|
339
|
+
ix[:indexes].each do |i|
|
|
340
|
+
flags = []
|
|
341
|
+
flags << "unique" if i[:unique]
|
|
342
|
+
flags << "sparse" if i[:sparse]
|
|
343
|
+
flags << "ttl=#{i[:expire_after_seconds]}" if i[:expire_after_seconds]
|
|
344
|
+
flags << "_id" if i[:implicit_id]
|
|
345
|
+
flags << "ops=#{i[:usage][:ops]}" if i[:usage]
|
|
346
|
+
suffix = flags.any? ? " [#{flags.join(", ")}]" : ""
|
|
347
|
+
lines << " - #{i[:name]} #{i[:key].inspect}#{suffix}"
|
|
348
|
+
end
|
|
349
|
+
if ix.key?(:usage_available)
|
|
350
|
+
lines << " usage: #{ix[:usage_available] ? "available" : "unavailable (role lacks clusterMonitor)"}"
|
|
351
|
+
end
|
|
352
|
+
if (drift = ix[:drift])
|
|
353
|
+
lines << " declared: #{ix[:declared].size}"
|
|
354
|
+
lines << " to_create: #{drift[:to_create].size}" if drift[:to_create].any?
|
|
355
|
+
lines << " in_sync: #{drift[:in_sync].size}" if drift[:in_sync].any?
|
|
356
|
+
lines << " orphans: #{drift[:orphans].inspect}" if drift[:orphans].any?
|
|
357
|
+
lines << " conflicts: #{drift[:conflicts].size}" if drift[:conflicts].any?
|
|
358
|
+
end
|
|
359
|
+
if (cap = ix[:capacity])
|
|
360
|
+
lines << " capacity: #{cap[:used]}/#{Parse::Core::Indexing::MAX_INDEXES_PER_COLLECTION} (#{cap[:remaining]} remaining)"
|
|
361
|
+
end
|
|
362
|
+
if (relations = ix[:relations])
|
|
363
|
+
lines << " relation_indexes:"
|
|
364
|
+
relations.each do |coll, info|
|
|
365
|
+
lines << " #{coll}"
|
|
366
|
+
lines << " declared: #{info[:declared].size}"
|
|
367
|
+
d = info[:drift]
|
|
368
|
+
lines << " to_create: #{d[:to_create].size}" if d[:to_create].any?
|
|
369
|
+
lines << " in_sync: #{d[:in_sync].size}" if d[:in_sync].any?
|
|
370
|
+
lines << " orphans: #{d[:orphans].inspect}" if d[:orphans].any?
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
else
|
|
374
|
+
lines << " indexes: unavailable (#{ix[:reason]})"
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
lines.join("\n")
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
end
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Parse
|
|
5
|
+
module Core
|
|
6
|
+
# Enhanced change tracking for Parse::Object that provides additional
|
|
7
|
+
# _was_changed? and enhanced _was methods for after_save hooks.
|
|
8
|
+
#
|
|
9
|
+
# This module adds _was_changed? methods that work correctly in after_save contexts
|
|
10
|
+
# by using previous_changes, while keeping normal _changed? methods intact.
|
|
11
|
+
#
|
|
12
|
+
# Key benefits:
|
|
13
|
+
# - _was_changed? methods work correctly in after_save hooks
|
|
14
|
+
# - _was methods return actual previous values (not current values) in after_save
|
|
15
|
+
# - Normal _changed? methods remain unchanged (standard ActiveModel behavior)
|
|
16
|
+
# - Automatically detects context using presence of previous_changes
|
|
17
|
+
#
|
|
18
|
+
# @example
|
|
19
|
+
# class Product < Parse::Object
|
|
20
|
+
# property :name, :string
|
|
21
|
+
# property :price, :float
|
|
22
|
+
#
|
|
23
|
+
# after_save :send_price_alert
|
|
24
|
+
#
|
|
25
|
+
# def send_price_alert
|
|
26
|
+
# if price_was_changed? && price_was < price
|
|
27
|
+
# AlertService.send("Price increased from $#{price_was} to $#{price}")
|
|
28
|
+
# end
|
|
29
|
+
# end
|
|
30
|
+
# end
|
|
31
|
+
module EnhancedChangeTracking
|
|
32
|
+
def self.included(base)
|
|
33
|
+
base.extend(ClassMethods)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
module ClassMethods
|
|
37
|
+
# Override the property method to add enhanced change tracking
|
|
38
|
+
# after the ActiveModel methods are defined
|
|
39
|
+
def property(key, data_type = :string, **opts)
|
|
40
|
+
result = super # Call the original property method
|
|
41
|
+
|
|
42
|
+
# After property is defined, override the _changed? and _was methods
|
|
43
|
+
enhance_change_tracking_for_field(key)
|
|
44
|
+
|
|
45
|
+
result
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
# Create enhanced versions of _was_changed? and _was methods for a field
|
|
51
|
+
# @param field_name [Symbol] the field name to enhance
|
|
52
|
+
def enhance_change_tracking_for_field(field_name)
|
|
53
|
+
was_changed_method = "#{field_name}_was_changed?"
|
|
54
|
+
was_method = "#{field_name}_was"
|
|
55
|
+
|
|
56
|
+
# Store reference to original _was method if it exists
|
|
57
|
+
# Only alias if not already aliased (prevents infinite recursion)
|
|
58
|
+
original_was_method = "__original_#{was_method}".to_sym
|
|
59
|
+
if instance_method_defined?(was_method) && !instance_method_defined?(original_was_method)
|
|
60
|
+
alias_method original_was_method, was_method
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Define enhanced _was_changed? method (for after_save context)
|
|
64
|
+
define_method(was_changed_method) do
|
|
65
|
+
enhanced_field_changed?(field_name.to_s)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Define enhanced _was method
|
|
69
|
+
define_method(was_method) do
|
|
70
|
+
enhanced_field_was(field_name.to_s)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Check if an instance method is defined
|
|
75
|
+
# @param method_name [String, Symbol] the method name
|
|
76
|
+
# @return [Boolean] true if the method is defined
|
|
77
|
+
def instance_method_defined?(method_name)
|
|
78
|
+
method_defined?(method_name) || private_method_defined?(method_name)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
# Enhanced implementation of field_changed? that works in all contexts
|
|
85
|
+
# @param field_name [String] the name of the field to check
|
|
86
|
+
# @return [Boolean] true if the field was changed, false otherwise
|
|
87
|
+
def enhanced_field_changed?(field_name)
|
|
88
|
+
# In before_save context: use current changes (ActiveModel's changed? method)
|
|
89
|
+
# In after_save context: use previous_changes to see what was just changed
|
|
90
|
+
if in_after_save_context?
|
|
91
|
+
# Use previous_changes for after_save hooks
|
|
92
|
+
if previous_changes_available?
|
|
93
|
+
return previous_changes.key?(field_name.to_s)
|
|
94
|
+
end
|
|
95
|
+
else
|
|
96
|
+
# Use original ActiveModel method for before_save hooks and general usage
|
|
97
|
+
original_method = "__original_#{field_name}_changed?".to_sym
|
|
98
|
+
if respond_to?(original_method, true)
|
|
99
|
+
return send(original_method)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Fallback: check if field is in current changes
|
|
103
|
+
return changed.include?(field_name.to_s) if respond_to?(:changed)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Default fallback
|
|
107
|
+
false
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Enhanced implementation of field_was that works in all contexts
|
|
111
|
+
# @param field_name [String] the name of the field to get previous value for
|
|
112
|
+
# @return [Object] the previous value of the field
|
|
113
|
+
def enhanced_field_was(field_name)
|
|
114
|
+
# In after_save context: use previous_changes to get what was just changed
|
|
115
|
+
if in_after_save_context?
|
|
116
|
+
if previous_changes_available? && previous_changes[field_name.to_s]
|
|
117
|
+
return previous_changes[field_name.to_s][0] # [old_value, new_value]
|
|
118
|
+
end
|
|
119
|
+
else
|
|
120
|
+
# In before_save context: use original ActiveModel method for current operation
|
|
121
|
+
original_method = "__original_#{field_name}_was".to_sym
|
|
122
|
+
if respond_to?(original_method, true)
|
|
123
|
+
return send(original_method)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Fallback: try to get from changes if field is currently changed
|
|
127
|
+
if respond_to?(:changes) && changes[field_name.to_s]
|
|
128
|
+
return changes[field_name.to_s][0] # [old_value, new_value]
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Default fallback to current value if no change detected
|
|
133
|
+
respond_to?(field_name) ? send(field_name) : nil
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Check if previous_changes is available and populated
|
|
137
|
+
# @return [Boolean] true if previous_changes is available
|
|
138
|
+
def previous_changes_available?
|
|
139
|
+
respond_to?(:previous_changes) &&
|
|
140
|
+
previous_changes.is_a?(Hash) &&
|
|
141
|
+
!previous_changes.empty?
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Detect if we're currently in an after_save context
|
|
145
|
+
# This is a heuristic based on the state of changes vs previous_changes
|
|
146
|
+
# @return [Boolean] true if likely in after_save context
|
|
147
|
+
def in_after_save_context?
|
|
148
|
+
# In after_save context:
|
|
149
|
+
# - previous_changes is populated (from the save that just completed)
|
|
150
|
+
# - current changes should be empty (cleared by successful save)
|
|
151
|
+
return false unless previous_changes_available?
|
|
152
|
+
return false unless respond_to?(:changed)
|
|
153
|
+
|
|
154
|
+
# If we have previous_changes but no current changes, we're likely in after_save
|
|
155
|
+
changed.empty?
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# The set of all Parse errors.
|
|
5
|
+
module Parse
|
|
6
|
+
# An abstract parent class for all Parse::Error types.
|
|
7
|
+
#
|
|
8
|
+
# Supports both legacy single-argument construction (`raise Parse::Error, "msg"`)
|
|
9
|
+
# and two-argument construction with a Parse error code
|
|
10
|
+
# (`raise Parse::Error.new(code, "msg")`). When a code is provided it is
|
|
11
|
+
# exposed via {#code} and prefixed onto the message.
|
|
12
|
+
class Error < StandardError
|
|
13
|
+
# @return [Integer, String, nil] the Parse error code when constructed with one.
|
|
14
|
+
attr_reader :code
|
|
15
|
+
|
|
16
|
+
def initialize(code_or_message = nil, message = nil)
|
|
17
|
+
if message.nil?
|
|
18
|
+
super(code_or_message)
|
|
19
|
+
else
|
|
20
|
+
@code = code_or_message
|
|
21
|
+
super("[#{code_or_message}] #{message}")
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Raised when attempting to access a field that was not fetched on a partially
|
|
27
|
+
# fetched object when autofetch has been disabled.
|
|
28
|
+
class UnfetchedFieldAccessError < Error
|
|
29
|
+
attr_reader :field_name, :object_class
|
|
30
|
+
|
|
31
|
+
def initialize(field_name, object_class)
|
|
32
|
+
@field_name = field_name
|
|
33
|
+
@object_class = object_class
|
|
34
|
+
super("Attempted to access unfetched field '#{field_name}' on #{object_class} with autofetch disabled. " \
|
|
35
|
+
"Either fetch the object first, include this field in the keys parameter, or enable autofetch.")
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|