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,563 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../stack.rb"
|
|
5
|
+
require "active_support"
|
|
6
|
+
require "active_support/inflector"
|
|
7
|
+
require "active_support/core_ext"
|
|
8
|
+
require "rake"
|
|
9
|
+
require "rake/dsl_definition"
|
|
10
|
+
|
|
11
|
+
module Parse
|
|
12
|
+
module Stack
|
|
13
|
+
# Loads and installs all Parse::Stack related tasks in a rake file.
|
|
14
|
+
def self.load_tasks
|
|
15
|
+
Parse::Stack::Tasks.new.install_tasks
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Defines all the related Rails tasks for Parse.
|
|
19
|
+
class Tasks
|
|
20
|
+
include Rake::DSL if defined? Rake::DSL
|
|
21
|
+
|
|
22
|
+
# Installs the rake tasks.
|
|
23
|
+
def install_tasks
|
|
24
|
+
if defined?(::Rails)
|
|
25
|
+
unless Rake::Task.task_defined?("db:seed") || Rails.root.blank?
|
|
26
|
+
namespace :db do
|
|
27
|
+
desc "Seeds your database with by loading db/seeds.rb"
|
|
28
|
+
task :seed => "parse:env" do
|
|
29
|
+
load Rails.root.join("db", "seeds.rb")
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
namespace :parse do
|
|
36
|
+
task :env do
|
|
37
|
+
if Rake::Task.task_defined?("environment")
|
|
38
|
+
Rake::Task["environment"].invoke
|
|
39
|
+
if defined?(::Rails)
|
|
40
|
+
Rails.application.eager_load! if Rails.application.present?
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
task :verify_env => :env do
|
|
46
|
+
unless Parse::Client.client?
|
|
47
|
+
raise "Please make sure you have setup the Parse.setup configuration before invoking task. Usually done in the :environment task."
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
endpoint = ENV["HOOKS_URL"] || ""
|
|
51
|
+
unless endpoint.starts_with?("http://") || endpoint.starts_with?("https://")
|
|
52
|
+
raise "The ENV variable HOOKS_URL must be a <http/s> url : '#{endpoint}'. Ex. https://12345678.ngrok.io/webhooks"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
desc "Run auto_upgrade on all of your Parse models."
|
|
57
|
+
task :upgrade => :env do
|
|
58
|
+
puts "Auto Upgrading Parse schemas..."
|
|
59
|
+
Parse.auto_upgrade! do |k|
|
|
60
|
+
puts "[+] #{k}"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
namespace :references do
|
|
65
|
+
# Enumerate every class that opted in to `parse_reference`. After
|
|
66
|
+
# `:env` runs (which eager-loads Rails apps), `Parse::Object`'s
|
|
67
|
+
# descendant set contains the full model graph; we filter to the
|
|
68
|
+
# subset that exposes the `_parse_reference_fields` class reader.
|
|
69
|
+
# @api private
|
|
70
|
+
find_parse_reference_classes = lambda do
|
|
71
|
+
klasses = Parse::Object.descendants.select do |k|
|
|
72
|
+
k.respond_to?(:_parse_reference_fields) &&
|
|
73
|
+
Array(k._parse_reference_fields).any? &&
|
|
74
|
+
k.respond_to?(:parse_class) &&
|
|
75
|
+
k.parse_class.present?
|
|
76
|
+
end
|
|
77
|
+
filter = ENV["CLASS"]
|
|
78
|
+
if filter.present?
|
|
79
|
+
klasses = klasses.select { |k| k.parse_class == filter || k.name == filter }
|
|
80
|
+
if klasses.empty?
|
|
81
|
+
warn "[parse:references] CLASS=#{filter} matched no class declaring parse_reference"
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
klasses.uniq.sort_by { |k| k.parse_class }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
desc "List every class that declares `parse_reference`."
|
|
88
|
+
task :list => :env do
|
|
89
|
+
klasses = find_parse_reference_classes.call
|
|
90
|
+
if klasses.empty?
|
|
91
|
+
puts "[parse:references] no classes declare parse_reference (CLASS filter: #{ENV["CLASS"].inspect})"
|
|
92
|
+
next
|
|
93
|
+
end
|
|
94
|
+
klasses.each do |k|
|
|
95
|
+
fields = Array(k._parse_reference_fields).map do |fn|
|
|
96
|
+
remote = (k.field_map[fn] || fn).to_s
|
|
97
|
+
fn == remote.to_sym ? fn.to_s : "#{fn} -> #{remote}"
|
|
98
|
+
end
|
|
99
|
+
puts "[#{k.parse_class}] #{fields.join(", ")}"
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
desc "Backfill missing parse_reference values. ENV: CLASS, BATCH_SIZE (default 100), DRY_RUN=true"
|
|
104
|
+
task :populate => :verify_env do
|
|
105
|
+
klasses = find_parse_reference_classes.call
|
|
106
|
+
if klasses.empty?
|
|
107
|
+
puts "[parse:references:populate] nothing to do"
|
|
108
|
+
next
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
batch_size = (ENV["BATCH_SIZE"] || "100").to_i
|
|
112
|
+
batch_size = 100 if batch_size <= 0
|
|
113
|
+
dry_run = ENV["DRY_RUN"].to_s.downcase == "true"
|
|
114
|
+
|
|
115
|
+
if dry_run
|
|
116
|
+
puts "[parse:references:populate] DRY_RUN=true — no writes will be issued"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
klasses.each do |klass|
|
|
120
|
+
fields = Array(klass._parse_reference_fields)
|
|
121
|
+
fields.each do |field_name|
|
|
122
|
+
populated_total = 0
|
|
123
|
+
scanned_total = 0
|
|
124
|
+
loops_without_progress = 0
|
|
125
|
+
loop do
|
|
126
|
+
# Query for records where the reference column is null/
|
|
127
|
+
# missing. Parse Server treats `{ field: null }` as a match
|
|
128
|
+
# for both "explicitly null" and "field absent". The result
|
|
129
|
+
# set shrinks as we populate, so a fixed limit without
|
|
130
|
+
# offset naturally walks the unpopulated tail.
|
|
131
|
+
objects = klass.query(field_name => nil).limit(batch_size).results
|
|
132
|
+
break if objects.empty?
|
|
133
|
+
scanned_total += objects.size
|
|
134
|
+
|
|
135
|
+
if dry_run
|
|
136
|
+
eligible = objects.count { |o| o.id.present? }
|
|
137
|
+
puts "[#{klass.parse_class}.#{field_name}] [dry-run] #{eligible}/#{objects.size} eligible (cumulative scanned: #{scanned_total})"
|
|
138
|
+
# Without a write, the query returns the same objects
|
|
139
|
+
# forever. Stop after one batch in dry-run mode.
|
|
140
|
+
break
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
updated = klass.populate_parse_references!(objects)
|
|
144
|
+
populated_total += updated.size
|
|
145
|
+
puts "[#{klass.parse_class}.#{field_name}] populated #{updated.size}/#{objects.size} (cumulative: #{populated_total})"
|
|
146
|
+
|
|
147
|
+
# Defensive: if a full batch came back but none were
|
|
148
|
+
# populated (e.g. every record lacks an objectId, or all
|
|
149
|
+
# writes failed), we'd loop forever. Stop after two
|
|
150
|
+
# consecutive no-progress batches.
|
|
151
|
+
if updated.empty?
|
|
152
|
+
loops_without_progress += 1
|
|
153
|
+
if loops_without_progress >= 2
|
|
154
|
+
warn "[#{klass.parse_class}.#{field_name}] aborting — 2 consecutive batches made no progress (objects may lack objectIds or saves are failing)"
|
|
155
|
+
break
|
|
156
|
+
end
|
|
157
|
+
else
|
|
158
|
+
loops_without_progress = 0
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
break if objects.size < batch_size
|
|
162
|
+
end
|
|
163
|
+
puts "[#{klass.parse_class}.#{field_name}] done — #{populated_total} populated, #{scanned_total} scanned"
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end # references
|
|
168
|
+
|
|
169
|
+
namespace :mongo do
|
|
170
|
+
namespace :indexes do
|
|
171
|
+
# Enumerate classes that declared at least one `mongo_index`.
|
|
172
|
+
# CLASS env var narrows to a single class by parse_class or
|
|
173
|
+
# Ruby class name. Mirrors the references-task pattern.
|
|
174
|
+
# @api private
|
|
175
|
+
find_indexed_classes = lambda do
|
|
176
|
+
klasses = Parse::Object.descendants.select do |k|
|
|
177
|
+
k.respond_to?(:mongo_index_declarations) &&
|
|
178
|
+
Array(k.mongo_index_declarations).any? &&
|
|
179
|
+
k.respond_to?(:parse_class) &&
|
|
180
|
+
k.parse_class.present?
|
|
181
|
+
end
|
|
182
|
+
filter = ENV["CLASS"]
|
|
183
|
+
if filter.present?
|
|
184
|
+
klasses = klasses.select { |k| k.parse_class == filter || k.name == filter }
|
|
185
|
+
if klasses.empty?
|
|
186
|
+
warn "[parse:mongo:indexes] CLASS=#{filter} matched no class declaring mongo_index"
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
klasses.uniq.sort_by(&:parse_class)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Print a per-collection plan section for one collection's
|
|
193
|
+
# plan Hash (a single value from the multi-collection plan).
|
|
194
|
+
print_one = lambda do |label, p|
|
|
195
|
+
puts " #{label}"
|
|
196
|
+
puts " capacity: #{p[:capacity_used]} existing / #{Parse::Core::Indexing::MAX_INDEXES_PER_COLLECTION} max " \
|
|
197
|
+
"(#{p[:capacity_remaining]} remaining additive, #{p[:capacity_remaining_with_drop]} remaining if DROP=true)"
|
|
198
|
+
if !p[:capacity_ok] && p[:capacity_ok_with_drop]
|
|
199
|
+
puts " STATUS: BLOCKED additive — would exceed 64-index cap; DROP=true would clear orphans and fit"
|
|
200
|
+
elsif !p[:capacity_ok]
|
|
201
|
+
puts " STATUS: BLOCKED — would exceed 64-index cap even with DROP=true"
|
|
202
|
+
end
|
|
203
|
+
unless p[:parse_managed].empty?
|
|
204
|
+
puts " managed: #{p[:parse_managed].inspect} (excluded from migration)"
|
|
205
|
+
end
|
|
206
|
+
if p[:to_create].any?
|
|
207
|
+
puts " to_create:"
|
|
208
|
+
p[:to_create].each do |d|
|
|
209
|
+
flags = d[:options].dup
|
|
210
|
+
name = flags.delete(:name) || "(auto)"
|
|
211
|
+
puts " + #{d[:keys].inspect} name=#{name} opts=#{flags.inspect}"
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
if p[:in_sync].any?
|
|
215
|
+
puts " in_sync:"
|
|
216
|
+
p[:in_sync].each { |d| puts " = #{d[:keys].inspect}" }
|
|
217
|
+
end
|
|
218
|
+
if p[:conflicts].any?
|
|
219
|
+
puts " conflicts: (operator action required — neither create nor drop is safe)"
|
|
220
|
+
p[:conflicts].each do |c|
|
|
221
|
+
puts " ! declared=#{c[:declared][:keys].inspect}"
|
|
222
|
+
puts " existing=#{c[:existing].inspect}"
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
if p[:orphans].any?
|
|
226
|
+
puts " orphans: #{p[:orphans].inspect}"
|
|
227
|
+
puts " ^^ WARNING: any index not declared via `mongo_index` and not in"
|
|
228
|
+
puts " PARSE_MANAGED_INDEX_PATTERNS is listed here. This includes"
|
|
229
|
+
puts " DBA-created diagnostic indexes, indexes from other Parse SDKs,"
|
|
230
|
+
puts " and MongoDB Atlas index recommendations. Under DROP=true these"
|
|
231
|
+
puts " indexes WILL BE DROPPED. To preserve an index, declare it with"
|
|
232
|
+
puts " `mongo_index :field, name: \"<index_name>\"` on the model."
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Print every plan in a class's migrator output (one entry
|
|
237
|
+
# per target collection — parent + any `_Join:*` relation).
|
|
238
|
+
print_plan = lambda do |klass|
|
|
239
|
+
plans = Parse::Schema::IndexMigrator.new(klass).plan
|
|
240
|
+
puts ""
|
|
241
|
+
puts "=" * 70
|
|
242
|
+
puts "#{klass.parse_class} (#{klass.name})"
|
|
243
|
+
plans.each do |coll, p|
|
|
244
|
+
print_one.call(coll, p)
|
|
245
|
+
end
|
|
246
|
+
plans
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
desc "Dry-run plan: declared mongo_index entries vs current MongoDB state. ENV: CLASS"
|
|
250
|
+
task :plan => :env do
|
|
251
|
+
klasses = find_indexed_classes.call
|
|
252
|
+
if klasses.empty?
|
|
253
|
+
puts "[parse:mongo:indexes:plan] no classes declare mongo_index (CLASS filter: #{ENV["CLASS"].inspect})"
|
|
254
|
+
next
|
|
255
|
+
end
|
|
256
|
+
unless Parse::MongoDB.respond_to?(:enabled?) && Parse::MongoDB.enabled?
|
|
257
|
+
warn "[parse:mongo:indexes:plan] Parse::MongoDB is not enabled. Existing-index reads will return empty; declared lists will still print."
|
|
258
|
+
end
|
|
259
|
+
klasses.each { |k| print_plan.call(k) }
|
|
260
|
+
puts ""
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
desc "Apply declared mongo_index changes. ENV: CLASS, DROP=true (drop orphans), ALLOW_SYSTEM_CLASSES=true"
|
|
264
|
+
task :apply => :env do
|
|
265
|
+
klasses = find_indexed_classes.call
|
|
266
|
+
if klasses.empty?
|
|
267
|
+
puts "[parse:mongo:indexes:apply] nothing to do"
|
|
268
|
+
next
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Re-state the triple gate up-front. The primitives will
|
|
272
|
+
# raise the same errors per call, but surfacing the
|
|
273
|
+
# message here gives operators a single readable failure
|
|
274
|
+
# instead of N stack traces.
|
|
275
|
+
unless Parse::MongoDB.respond_to?(:writer_configured?) && Parse::MongoDB.writer_configured?
|
|
276
|
+
raise "[parse:mongo:indexes:apply] writer is not configured. " \
|
|
277
|
+
"Set up Parse::MongoDB.configure_writer(uri: ENV['MONGO_WRITER_URI']) in a rake initializer."
|
|
278
|
+
end
|
|
279
|
+
unless Parse::MongoDB.index_mutations_enabled
|
|
280
|
+
raise "[parse:mongo:indexes:apply] Parse::MongoDB.index_mutations_enabled is false. " \
|
|
281
|
+
"Set it to true explicitly in the rake initializer for this task."
|
|
282
|
+
end
|
|
283
|
+
unless ENV[Parse::MongoDB::MUTATION_ENV_KEY] == "1"
|
|
284
|
+
raise "[parse:mongo:indexes:apply] ENV[#{Parse::MongoDB::MUTATION_ENV_KEY.inspect}] must be \"1\"."
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
drop = ENV["DROP"].to_s.downcase == "true"
|
|
288
|
+
puts "[parse:mongo:indexes:apply] mode: #{drop ? "additive + drop-orphans" : "additive only"}"
|
|
289
|
+
if drop
|
|
290
|
+
puts ""
|
|
291
|
+
puts " !!! DROP=true is set. Every index listed under 'orphans:' below WILL BE DROPPED."
|
|
292
|
+
puts " Orphans include any index that does NOT match PARSE_MANAGED_INDEX_PATTERNS"
|
|
293
|
+
puts " and is NOT declared via `mongo_index` on the model. This may capture"
|
|
294
|
+
puts " DBA-created diagnostic indexes, indexes created by other SDKs, and MongoDB"
|
|
295
|
+
puts " Atlas index recommendations. Review the plan output below carefully before"
|
|
296
|
+
puts " proceeding. Cancel with Ctrl-C if anything in 'orphans:' is unexpected."
|
|
297
|
+
puts ""
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
klasses.each do |klass|
|
|
301
|
+
print_plan.call(klass)
|
|
302
|
+
results = Parse::Schema::IndexMigrator.new(klass).apply!(drop: drop)
|
|
303
|
+
puts ""
|
|
304
|
+
puts "[#{klass.parse_class}] applied:"
|
|
305
|
+
results.each do |coll, result|
|
|
306
|
+
puts " #{coll}:"
|
|
307
|
+
if result[:capacity_blocked]
|
|
308
|
+
warn " SKIPPED — capacity would be exceeded"
|
|
309
|
+
next
|
|
310
|
+
end
|
|
311
|
+
puts " created: #{result[:created].size}"
|
|
312
|
+
result[:created].each { |d| puts " + #{d[:keys].inspect}" }
|
|
313
|
+
unless result[:skipped_exists].empty?
|
|
314
|
+
puts " skipped_exists: #{result[:skipped_exists].size}"
|
|
315
|
+
result[:skipped_exists].each { |d| puts " = #{d[:keys].inspect}" }
|
|
316
|
+
end
|
|
317
|
+
if drop && !result[:dropped].empty?
|
|
318
|
+
puts " dropped: #{result[:dropped].inspect}"
|
|
319
|
+
end
|
|
320
|
+
unless result[:conflicts].empty?
|
|
321
|
+
warn " conflicts unresolved: #{result[:conflicts].size}"
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
puts ""
|
|
326
|
+
end
|
|
327
|
+
end # indexes
|
|
328
|
+
|
|
329
|
+
namespace :search_indexes do
|
|
330
|
+
# Enumerate classes that declared at least one
|
|
331
|
+
# `mongo_search_index`. CLASS env var narrows by parse_class
|
|
332
|
+
# or Ruby class name. Parallels find_indexed_classes in the
|
|
333
|
+
# regular-index task.
|
|
334
|
+
# @api private
|
|
335
|
+
find_search_indexed_classes = lambda do
|
|
336
|
+
klasses = Parse::Object.descendants.select do |k|
|
|
337
|
+
k.respond_to?(:mongo_search_index_declarations) &&
|
|
338
|
+
Array(k.mongo_search_index_declarations).any? &&
|
|
339
|
+
k.respond_to?(:parse_class) &&
|
|
340
|
+
k.parse_class.present?
|
|
341
|
+
end
|
|
342
|
+
filter = ENV["CLASS"]
|
|
343
|
+
if filter.present?
|
|
344
|
+
klasses = klasses.select { |k| k.parse_class == filter || k.name == filter }
|
|
345
|
+
if klasses.empty?
|
|
346
|
+
warn "[parse:mongo:search_indexes] CLASS=#{filter} matched no class declaring mongo_search_index"
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
klasses.uniq.sort_by(&:parse_class)
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# Print one model's search-index plan.
|
|
353
|
+
print_search_plan = lambda do |klass|
|
|
354
|
+
p = Parse::Schema::SearchIndexMigrator.new(klass).plan
|
|
355
|
+
puts ""
|
|
356
|
+
puts "=" * 70
|
|
357
|
+
puts "#{klass.parse_class} (#{klass.name})"
|
|
358
|
+
puts " collection: #{p[:collection]}"
|
|
359
|
+
unless p[:atlas_available]
|
|
360
|
+
puts " STATUS: atlas unavailable — `$listSearchIndexes` returned no data"
|
|
361
|
+
puts " every declared index will be reported as to_create"
|
|
362
|
+
end
|
|
363
|
+
puts " declared: #{p[:declared].size}"
|
|
364
|
+
if p[:to_create].any?
|
|
365
|
+
puts " to_create:"
|
|
366
|
+
p[:to_create].each { |d| puts " + #{d[:name].inspect} type=#{d[:type]}" }
|
|
367
|
+
end
|
|
368
|
+
if p[:in_sync].any?
|
|
369
|
+
puts " in_sync:"
|
|
370
|
+
p[:in_sync].each { |d| puts " = #{d[:name].inspect}" }
|
|
371
|
+
end
|
|
372
|
+
if p[:drifted].any?
|
|
373
|
+
puts " drifted: (definition differs from atlas latestDefinition)"
|
|
374
|
+
p[:drifted].each do |entry|
|
|
375
|
+
puts " ~ #{entry[:declared][:name].inspect} existing.status=#{entry[:existing][:status]}"
|
|
376
|
+
end
|
|
377
|
+
puts " ^^ NOT updated by default. Pass UPDATE=true to rebuild."
|
|
378
|
+
end
|
|
379
|
+
if p[:orphans].any?
|
|
380
|
+
puts " orphans: #{p[:orphans].inspect}"
|
|
381
|
+
puts " ^^ search indexes present on the collection but not declared."
|
|
382
|
+
puts " Pass DROP=true to drop them. (Atlas Search has a separate"
|
|
383
|
+
puts " per-cluster quota — orphans don't count against the regular"
|
|
384
|
+
puts " 64-index Mongo cap, but they do consume that quota.)"
|
|
385
|
+
end
|
|
386
|
+
p
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
desc "Dry-run plan: declared mongo_search_index entries vs current Atlas state. ENV: CLASS"
|
|
390
|
+
task :plan => :env do
|
|
391
|
+
klasses = find_search_indexed_classes.call
|
|
392
|
+
if klasses.empty?
|
|
393
|
+
puts "[parse:mongo:search_indexes:plan] no classes declare mongo_search_index (CLASS filter: #{ENV["CLASS"].inspect})"
|
|
394
|
+
next
|
|
395
|
+
end
|
|
396
|
+
unless Parse::MongoDB.respond_to?(:enabled?) && Parse::MongoDB.enabled?
|
|
397
|
+
warn "[parse:mongo:search_indexes:plan] Parse::MongoDB is not enabled — existing-index reads will be empty."
|
|
398
|
+
end
|
|
399
|
+
klasses.each { |k| print_search_plan.call(k) }
|
|
400
|
+
puts ""
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
desc "Apply declared mongo_search_index changes. ENV: CLASS, UPDATE=true (rebuild drifted), DROP=true (drop orphans), WAIT=true (block until READY), WAIT_TIMEOUT=600"
|
|
404
|
+
task :apply => :env do
|
|
405
|
+
klasses = find_search_indexed_classes.call
|
|
406
|
+
if klasses.empty?
|
|
407
|
+
puts "[parse:mongo:search_indexes:apply] nothing to do"
|
|
408
|
+
next
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
# Triple gate — re-state up-front for one readable
|
|
412
|
+
# failure instead of N stack traces from the primitives.
|
|
413
|
+
unless Parse::MongoDB.respond_to?(:writer_configured?) && Parse::MongoDB.writer_configured?
|
|
414
|
+
raise "[parse:mongo:search_indexes:apply] writer is not configured. " \
|
|
415
|
+
"Set up Parse::MongoDB.configure_writer(uri: ENV['MONGO_WRITER_URI']) in a rake initializer."
|
|
416
|
+
end
|
|
417
|
+
unless Parse::MongoDB.index_mutations_enabled
|
|
418
|
+
raise "[parse:mongo:search_indexes:apply] Parse::MongoDB.index_mutations_enabled is false. " \
|
|
419
|
+
"Set it to true explicitly in the rake initializer for this task."
|
|
420
|
+
end
|
|
421
|
+
unless ENV[Parse::MongoDB::MUTATION_ENV_KEY] == "1"
|
|
422
|
+
raise "[parse:mongo:search_indexes:apply] ENV[#{Parse::MongoDB::MUTATION_ENV_KEY.inspect}] must be \"1\"."
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
update = ENV["UPDATE"].to_s.downcase == "true"
|
|
426
|
+
drop = ENV["DROP"].to_s.downcase == "true"
|
|
427
|
+
wait = ENV["WAIT"].to_s.downcase == "true"
|
|
428
|
+
timeout = (ENV["WAIT_TIMEOUT"] || "600").to_i
|
|
429
|
+
modes = []
|
|
430
|
+
modes << "additive"
|
|
431
|
+
modes << "update-drifted" if update
|
|
432
|
+
modes << "drop-orphans" if drop
|
|
433
|
+
modes << "wait-for-ready (#{timeout}s)" if wait
|
|
434
|
+
puts "[parse:mongo:search_indexes:apply] mode: #{modes.join(" + ")}"
|
|
435
|
+
if drop
|
|
436
|
+
puts ""
|
|
437
|
+
puts " !!! DROP=true is set. Every search index listed under 'orphans:' below WILL BE DROPPED."
|
|
438
|
+
puts ""
|
|
439
|
+
end
|
|
440
|
+
if update
|
|
441
|
+
puts ""
|
|
442
|
+
puts " !!! UPDATE=true is set. Every search index listed under 'drifted:' WILL BE REBUILT."
|
|
443
|
+
puts " Atlas Search rebuilds run asynchronously; queries hit the old definition until READY."
|
|
444
|
+
puts ""
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
klasses.each do |klass|
|
|
448
|
+
print_search_plan.call(klass)
|
|
449
|
+
results = Parse::Schema::SearchIndexMigrator.new(klass).apply!(
|
|
450
|
+
update: update, drop: drop, wait: wait, timeout: timeout,
|
|
451
|
+
)
|
|
452
|
+
puts ""
|
|
453
|
+
puts "[#{klass.parse_class}] applied:"
|
|
454
|
+
puts " created: #{results[:created].size}"
|
|
455
|
+
results[:created].each { |d| puts " + #{d[:name]}" }
|
|
456
|
+
unless results[:skipped_exists].empty?
|
|
457
|
+
puts " skipped_exists: #{results[:skipped_exists].size}"
|
|
458
|
+
results[:skipped_exists].each { |d| puts " = #{d[:name]} (raced — already present at apply time)" }
|
|
459
|
+
end
|
|
460
|
+
unless results[:in_sync].empty?
|
|
461
|
+
puts " in_sync: #{results[:in_sync].size}"
|
|
462
|
+
end
|
|
463
|
+
if update && !results[:updated].empty?
|
|
464
|
+
puts " updated: #{results[:updated].inspect}"
|
|
465
|
+
elsif !results[:drifted_skipped].empty?
|
|
466
|
+
puts " drifted_skipped: #{results[:drifted_skipped].inspect} (pass UPDATE=true to rebuild)"
|
|
467
|
+
end
|
|
468
|
+
if drop && !results[:dropped].empty?
|
|
469
|
+
puts " dropped: #{results[:dropped].inspect}"
|
|
470
|
+
elsif !results[:orphans_skipped].empty?
|
|
471
|
+
puts " orphans_skipped: #{results[:orphans_skipped].inspect} (pass DROP=true to remove)"
|
|
472
|
+
end
|
|
473
|
+
unless results[:wait_results].empty?
|
|
474
|
+
puts " wait_results:"
|
|
475
|
+
results[:wait_results].each { |name, outcome| puts " #{name}: #{outcome}" }
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
puts ""
|
|
479
|
+
end
|
|
480
|
+
end # search_indexes
|
|
481
|
+
end # mongo
|
|
482
|
+
|
|
483
|
+
namespace :webhooks do
|
|
484
|
+
desc "Register local webhooks with Parse server"
|
|
485
|
+
task :register => :verify_env do
|
|
486
|
+
endpoint = ENV["HOOKS_URL"]
|
|
487
|
+
puts "Registering Parse Webhooks @ #{endpoint}"
|
|
488
|
+
Rake::Task["parse:webhooks:register:functions"].invoke
|
|
489
|
+
Rake::Task["parse:webhooks:register:triggers"].invoke
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
desc "List all webhooks and triggers registered with the Parse Server"
|
|
493
|
+
task :list => :verify_env do
|
|
494
|
+
Rake::Task["parse:webhooks:list:functions"].invoke
|
|
495
|
+
Rake::Task["parse:webhooks:list:triggers"].invoke
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
desc "Remove all locally registered webhooks from the Parse Application."
|
|
499
|
+
task :remove => :verify_env do
|
|
500
|
+
Rake::Task["parse:webhooks:remove:functions"].invoke
|
|
501
|
+
Rake::Task["parse:webhooks:remove:triggers"].invoke
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
namespace :list do
|
|
505
|
+
task :functions => :verify_env do
|
|
506
|
+
endpoint = ENV["HOOKS_URL"] || "-"
|
|
507
|
+
Parse.client.functions.each do |r|
|
|
508
|
+
name = r["functionName"]
|
|
509
|
+
url = r["url"]
|
|
510
|
+
star = url.starts_with?(endpoint) ? "*" : " "
|
|
511
|
+
puts "[#{star}] #{name} -> #{url}"
|
|
512
|
+
end
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
task :triggers => :verify_env do
|
|
516
|
+
endpoint = ENV["HOOKS_URL"] || "-"
|
|
517
|
+
triggers = Parse.client.triggers.results
|
|
518
|
+
triggers.sort! { |x, y| [x["className"], x["triggerName"]] <=> [y["className"], y["triggerName"]] }
|
|
519
|
+
triggers.each do |r|
|
|
520
|
+
name = r["className"]
|
|
521
|
+
trigger = r["triggerName"]
|
|
522
|
+
url = r["url"]
|
|
523
|
+
star = url.starts_with?(endpoint) ? "*" : " "
|
|
524
|
+
puts "[#{star}] #{name}.#{trigger} -> #{url}"
|
|
525
|
+
end
|
|
526
|
+
end
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
namespace :register do
|
|
530
|
+
task :functions => :verify_env do
|
|
531
|
+
endpoint = ENV["HOOKS_URL"]
|
|
532
|
+
Parse::Webhooks.register_functions!(endpoint) do |name|
|
|
533
|
+
puts "[+] function - #{name}"
|
|
534
|
+
end
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
task :triggers => :verify_env do
|
|
538
|
+
endpoint = ENV["HOOKS_URL"]
|
|
539
|
+
Parse::Webhooks.register_triggers!(endpoint, **{ include_wildcard: true }) do |trigger, name|
|
|
540
|
+
puts "[+] #{trigger.to_s.ljust(12, " ")} - #{name}"
|
|
541
|
+
end
|
|
542
|
+
end
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
namespace :remove do
|
|
546
|
+
task :functions => :verify_env do
|
|
547
|
+
Parse::Webhooks.remove_all_functions! do |name|
|
|
548
|
+
puts "[-] function - #{name}"
|
|
549
|
+
end
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
task :triggers => :verify_env do
|
|
553
|
+
Parse::Webhooks.remove_all_triggers! do |trigger, name|
|
|
554
|
+
puts "[-] #{trigger.to_s.ljust(12, " ")} - #{name}"
|
|
555
|
+
end
|
|
556
|
+
end
|
|
557
|
+
end
|
|
558
|
+
end # webhooks
|
|
559
|
+
end # webhooks namespace
|
|
560
|
+
end
|
|
561
|
+
end # Tasks
|
|
562
|
+
end # Webhooks
|
|
563
|
+
end # Parse
|