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.
Files changed (178) hide show
  1. checksums.yaml +7 -0
  2. data/.bundle/config +2 -0
  3. data/.env.sample +112 -0
  4. data/.env.test +10 -0
  5. data/.github/workflows/ruby.yml +36 -0
  6. data/.gitignore +49 -0
  7. data/.ruby-version +1 -0
  8. data/.solargraph.yml +22 -0
  9. data/CHANGELOG.md +5816 -0
  10. data/Gemfile +30 -0
  11. data/Gemfile.lock +175 -0
  12. data/LICENSE.txt +23 -0
  13. data/Makefile +63 -0
  14. data/README.md +5655 -0
  15. data/Rakefile +573 -0
  16. data/bin/console +38 -0
  17. data/bin/parse-console +136 -0
  18. data/bin/server +17 -0
  19. data/bin/setup +7 -0
  20. data/config/parse-config.json +12 -0
  21. data/docs/TEST_SERVER.md +271 -0
  22. data/docs/_config.yml +1 -0
  23. data/docs/mcp_guide.md +3484 -0
  24. data/docs/mongodb_direct_guide.md +1348 -0
  25. data/docs/mongodb_index_optimization_guide.md +631 -0
  26. data/examples/transaction_example.rb +219 -0
  27. data/lib/parse/acl_scope.rb +728 -0
  28. data/lib/parse/agent/cancellation_token.rb +80 -0
  29. data/lib/parse/agent/constraint_translator.rb +480 -0
  30. data/lib/parse/agent/describe.rb +420 -0
  31. data/lib/parse/agent/errors.rb +133 -0
  32. data/lib/parse/agent/mcp_client.rb +557 -0
  33. data/lib/parse/agent/mcp_dispatcher.rb +1023 -0
  34. data/lib/parse/agent/mcp_rack_app.rb +1143 -0
  35. data/lib/parse/agent/mcp_server.rb +376 -0
  36. data/lib/parse/agent/metadata_audit.rb +259 -0
  37. data/lib/parse/agent/metadata_dsl.rb +733 -0
  38. data/lib/parse/agent/metadata_registry.rb +794 -0
  39. data/lib/parse/agent/pipeline_validator.rb +82 -0
  40. data/lib/parse/agent/prompts.rb +351 -0
  41. data/lib/parse/agent/rate_limiter.rb +158 -0
  42. data/lib/parse/agent/relation_graph.rb +162 -0
  43. data/lib/parse/agent/result_formatter.rb +453 -0
  44. data/lib/parse/agent/tools.rb +5489 -0
  45. data/lib/parse/agent.rb +3249 -0
  46. data/lib/parse/api/aggregate.rb +79 -0
  47. data/lib/parse/api/all.rb +26 -0
  48. data/lib/parse/api/analytics.rb +18 -0
  49. data/lib/parse/api/batch.rb +33 -0
  50. data/lib/parse/api/cloud_functions.rb +58 -0
  51. data/lib/parse/api/config.rb +125 -0
  52. data/lib/parse/api/files.rb +29 -0
  53. data/lib/parse/api/hooks.rb +117 -0
  54. data/lib/parse/api/objects.rb +146 -0
  55. data/lib/parse/api/path_segment.rb +75 -0
  56. data/lib/parse/api/push.rb +20 -0
  57. data/lib/parse/api/schema.rb +49 -0
  58. data/lib/parse/api/server.rb +50 -0
  59. data/lib/parse/api/sessions.rb +24 -0
  60. data/lib/parse/api/users.rb +250 -0
  61. data/lib/parse/atlas_search/index_manager.rb +353 -0
  62. data/lib/parse/atlas_search/result.rb +204 -0
  63. data/lib/parse/atlas_search/search_builder.rb +604 -0
  64. data/lib/parse/atlas_search/session.rb +253 -0
  65. data/lib/parse/atlas_search.rb +995 -0
  66. data/lib/parse/client/authentication.rb +97 -0
  67. data/lib/parse/client/batch.rb +234 -0
  68. data/lib/parse/client/body_builder.rb +240 -0
  69. data/lib/parse/client/caching.rb +203 -0
  70. data/lib/parse/client/logging.rb +293 -0
  71. data/lib/parse/client/profiling.rb +181 -0
  72. data/lib/parse/client/protocol.rb +91 -0
  73. data/lib/parse/client/request.rb +233 -0
  74. data/lib/parse/client/response.rb +208 -0
  75. data/lib/parse/client.rb +1104 -0
  76. data/lib/parse/clp_scope.rb +361 -0
  77. data/lib/parse/live_query/circuit_breaker.rb +256 -0
  78. data/lib/parse/live_query/client.rb +1001 -0
  79. data/lib/parse/live_query/configuration.rb +224 -0
  80. data/lib/parse/live_query/event.rb +115 -0
  81. data/lib/parse/live_query/event_queue.rb +272 -0
  82. data/lib/parse/live_query/health_monitor.rb +214 -0
  83. data/lib/parse/live_query/logging.rb +149 -0
  84. data/lib/parse/live_query/subscription.rb +294 -0
  85. data/lib/parse/live_query.rb +163 -0
  86. data/lib/parse/lookup_rewriter.rb +445 -0
  87. data/lib/parse/model/acl.rb +968 -0
  88. data/lib/parse/model/associations/belongs_to.rb +275 -0
  89. data/lib/parse/model/associations/collection_proxy.rb +435 -0
  90. data/lib/parse/model/associations/has_many.rb +597 -0
  91. data/lib/parse/model/associations/has_one.rb +158 -0
  92. data/lib/parse/model/associations/pointer_collection_proxy.rb +134 -0
  93. data/lib/parse/model/associations/relation_collection_proxy.rb +177 -0
  94. data/lib/parse/model/bytes.rb +62 -0
  95. data/lib/parse/model/classes/audience.rb +262 -0
  96. data/lib/parse/model/classes/installation.rb +363 -0
  97. data/lib/parse/model/classes/job_schedule.rb +153 -0
  98. data/lib/parse/model/classes/job_status.rb +264 -0
  99. data/lib/parse/model/classes/product.rb +75 -0
  100. data/lib/parse/model/classes/push_status.rb +263 -0
  101. data/lib/parse/model/classes/role.rb +751 -0
  102. data/lib/parse/model/classes/session.rb +201 -0
  103. data/lib/parse/model/classes/user.rb +943 -0
  104. data/lib/parse/model/clp.rb +544 -0
  105. data/lib/parse/model/core/actions.rb +1268 -0
  106. data/lib/parse/model/core/builder.rb +139 -0
  107. data/lib/parse/model/core/create_lock.rb +386 -0
  108. data/lib/parse/model/core/describe.rb +382 -0
  109. data/lib/parse/model/core/enhanced_change_tracking.rb +159 -0
  110. data/lib/parse/model/core/errors.rb +38 -0
  111. data/lib/parse/model/core/fetching.rb +566 -0
  112. data/lib/parse/model/core/field_guards.rb +220 -0
  113. data/lib/parse/model/core/indexing.rb +382 -0
  114. data/lib/parse/model/core/parse_reference.rb +407 -0
  115. data/lib/parse/model/core/properties.rb +809 -0
  116. data/lib/parse/model/core/querying.rb +491 -0
  117. data/lib/parse/model/core/schema.rb +202 -0
  118. data/lib/parse/model/core/search_indexing.rb +174 -0
  119. data/lib/parse/model/date.rb +88 -0
  120. data/lib/parse/model/email.rb +213 -0
  121. data/lib/parse/model/file.rb +527 -0
  122. data/lib/parse/model/geojson.rb +271 -0
  123. data/lib/parse/model/geopoint.rb +261 -0
  124. data/lib/parse/model/model.rb +260 -0
  125. data/lib/parse/model/object.rb +2068 -0
  126. data/lib/parse/model/phone.rb +520 -0
  127. data/lib/parse/model/pointer.rb +443 -0
  128. data/lib/parse/model/polygon.rb +406 -0
  129. data/lib/parse/model/push.rb +975 -0
  130. data/lib/parse/model/shortnames.rb +8 -0
  131. data/lib/parse/model/time_zone.rb +141 -0
  132. data/lib/parse/model/validations/uniqueness_validator.rb +97 -0
  133. data/lib/parse/model/validations.rb +96 -0
  134. data/lib/parse/mongodb.rb +2300 -0
  135. data/lib/parse/pipeline_security.rb +554 -0
  136. data/lib/parse/query/constraint.rb +198 -0
  137. data/lib/parse/query/constraints.rb +3279 -0
  138. data/lib/parse/query/cursor.rb +434 -0
  139. data/lib/parse/query/n_plus_one_detector.rb +445 -0
  140. data/lib/parse/query/operation.rb +104 -0
  141. data/lib/parse/query/ordering.rb +66 -0
  142. data/lib/parse/query.rb +7028 -0
  143. data/lib/parse/schema/index_migrator.rb +291 -0
  144. data/lib/parse/schema/search_index_migrator.rb +289 -0
  145. data/lib/parse/schema.rb +494 -0
  146. data/lib/parse/stack/generators/rails.rb +40 -0
  147. data/lib/parse/stack/generators/templates/model.erb +51 -0
  148. data/lib/parse/stack/generators/templates/model_installation.rb +4 -0
  149. data/lib/parse/stack/generators/templates/model_role.rb +4 -0
  150. data/lib/parse/stack/generators/templates/model_session.rb +4 -0
  151. data/lib/parse/stack/generators/templates/model_user.rb +11 -0
  152. data/lib/parse/stack/generators/templates/parse.rb +12 -0
  153. data/lib/parse/stack/generators/templates/webhooks.rb +10 -0
  154. data/lib/parse/stack/railtie.rb +18 -0
  155. data/lib/parse/stack/tasks.rb +563 -0
  156. data/lib/parse/stack/version.rb +11 -0
  157. data/lib/parse/stack.rb +455 -0
  158. data/lib/parse/two_factor_auth/user_extension.rb +449 -0
  159. data/lib/parse/two_factor_auth.rb +310 -0
  160. data/lib/parse/webhooks/payload.rb +360 -0
  161. data/lib/parse/webhooks/registration.rb +199 -0
  162. data/lib/parse/webhooks/replay_protection.rb +189 -0
  163. data/lib/parse/webhooks.rb +510 -0
  164. data/lib/parse-stack-next.rb +5 -0
  165. data/lib/parse-stack.rb +5 -0
  166. data/parse-stack-next.gemspec +82 -0
  167. data/parse-stack.png +0 -0
  168. data/scripts/debug-ips.js +35 -0
  169. data/scripts/docker/Dockerfile.parse +13 -0
  170. data/scripts/docker/atlas-init.js +284 -0
  171. data/scripts/docker/docker-compose.atlas.yml +76 -0
  172. data/scripts/docker/docker-compose.test.yml +106 -0
  173. data/scripts/docker/mongo-init.js +21 -0
  174. data/scripts/eval_mcp_with_lm_studio.rb +274 -0
  175. data/scripts/start-parse.sh +90 -0
  176. data/scripts/start_mcp_server.rb +78 -0
  177. data/scripts/test_server_connection.rb +82 -0
  178. 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
@@ -0,0 +1,11 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ module Parse
5
+ # @author Anthony Persaud, Henry Spindell, Adrian Curtin
6
+ # The Parse Server SDK for Ruby
7
+ module Stack
8
+ # The current version.
9
+ VERSION = "4.5.0"
10
+ end
11
+ end