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,491 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../../query"
5
+
6
+ module Parse
7
+ module Core
8
+ # Defines the querying methods applied to a Parse::Object.
9
+ module Querying
10
+
11
+ # This feature is a small subset of the
12
+ # {http://guides.rubyonrails.org/active_record_querying.html#scopes
13
+ # ActiveRecord named scopes} feature. Scoping allows you to specify
14
+ # commonly-used queries which can be referenced as class method calls and
15
+ # are chainable with other scopes. You can use every {Parse::Query}
16
+ # method previously covered such as `where`, `includes` and `limit`.
17
+ #
18
+ # class Article < Parse::Object
19
+ # property :published, :boolean
20
+ # scope :published, -> { query(published: true) }
21
+ # end
22
+ #
23
+ # This is the same as defining your own class method for the query.
24
+ #
25
+ # class Article < Parse::Object
26
+ # def self.published
27
+ # query(published: true)
28
+ # end
29
+ # end
30
+ #
31
+ # You can also chain scopes and pass parameters. In addition, boolean and
32
+ # enumerated properties have automatically generated scopes for you to use.
33
+ #
34
+ # class Article < Parse::Object
35
+ # scope :published, -> { query(published: true) }
36
+ #
37
+ # property :comment_count, :integer
38
+ # property :category
39
+ # property :approved, :boolean
40
+ #
41
+ # scope :published_and_commented, -> { published.where :comment_count.gt => 0 }
42
+ # scope :popular_topics, ->(name) { published_and_commented.where category: name }
43
+ # end
44
+ #
45
+ # # simple scope
46
+ # Article.published # => where published is true
47
+ #
48
+ # # chained scope
49
+ # Article.published_and_commented # published is true and comment_count > 0
50
+ #
51
+ # # scope with parameters
52
+ # Article.popular_topic("music") # => popular music articles
53
+ # # equivalent: where(published: true, :comment_count.gt => 0, category: name)
54
+ #
55
+ # # automatically generated scope
56
+ # Article.approved(category: "tour") # => where approved: true, category: 'tour'
57
+ #
58
+ # If you would like to turn off automatic scope generation for property types,
59
+ # set the option `:scope` to false when declaring the property.
60
+ # @param name [Symbol] the name of the scope.
61
+ # @param body [Proc] the proc related to the scope.
62
+ # @raise ArgumentError if body parameter does not respond to `call`
63
+ # @return [Symbol] the name of the singleton method created.
64
+ def scope(name, body)
65
+ unless body.respond_to?(:call)
66
+ raise ArgumentError, "The scope body needs to be callable."
67
+ end
68
+
69
+ name = name.to_sym
70
+ if respond_to?(name, true)
71
+ puts "Creating scope :#{name}. Will overwrite existing method #{self}.#{name}."
72
+ end
73
+
74
+ define_singleton_method(name) do |*args, &block|
75
+ if body.arity.zero?
76
+ res = body.call
77
+ res.conditions(*args) if args.present?
78
+ else
79
+ res = body.call(*args)
80
+ end
81
+
82
+ _q = res || query
83
+
84
+ if _q.is_a?(Parse::Query)
85
+ klass = self
86
+ _q.define_singleton_method(:method_missing) do |m, *args, &chained_block|
87
+ if klass.respond_to?(m, true)
88
+ # must be a scope
89
+ klass_scope = klass.send(m, *args)
90
+ if klass_scope.is_a?(Parse::Query)
91
+ # merge constraints
92
+ add_constraints(klass_scope.constraints)
93
+ # if a block was passed, execute the query, otherwise return the query
94
+ return chained_block.present? ? results(&chained_block) : self
95
+ end # if
96
+ klass = nil # help clean up ruby gc
97
+ return klass_scope
98
+ end
99
+ klass = nil # help clean up ruby gc
100
+ return results.send(m, *args, &chained_block)
101
+ end
102
+ end
103
+
104
+ Parse::Query.apply_auto_introspection!(_q)
105
+
106
+ return _q if block.nil?
107
+ _q.results(&block)
108
+ end
109
+ end
110
+
111
+ # Creates a new {Parse::Query} with the given constraints for this class.
112
+ # @example
113
+ # # assume Post < Parse::Object
114
+ # query = Post.query(:updated_at.before => DateTime.now)
115
+ # @return [Parse::Query] a new query with the given constraints for this
116
+ # Parse::Object subclass.
117
+ def query(constraints = {})
118
+ Parse::Query.new self.parse_class, constraints
119
+ end
120
+
121
+ alias_method :where, :query
122
+
123
+ # @param conditions (see Parse::Query#where)
124
+ # @return (see Parse::Query#where)
125
+ # @see Parse::Query#where
126
+ def literal_where(conditions = {})
127
+ query.where(conditions)
128
+ end
129
+
130
+ # This methods allow you to efficiently iterate over all the records in the collection
131
+ # (lower memory cost) at a minor cost of performance. This method utilizes
132
+ # the `created_at` field of Parse records to order and iterate over *all* matching records,
133
+ # therefore you should not use this method if you want to perform a query
134
+ # with constraints against the `created_at` field or need specific type of ordering.
135
+ # If you need to use `:created_at` in your constraints, consider using {Parse::Core::Querying#all} or
136
+ # {Parse::Core::Actions::ClassMethods#save_all}
137
+ # @param constraints [Hash] a set of query constraints.
138
+ # @yield a block which will iterate through each matching record.
139
+ # @example
140
+ #
141
+ # post = Post.first
142
+ # # iterate over all comments matching conditions
143
+ # Comment.each(post: post) do |comment|
144
+ # # ...
145
+ # end
146
+ # @return [Parse::Object] the last Parse::Object record processed.
147
+ # @note You cannot use *:created_at* as a constraint.
148
+ # @raise ArgumentError if :created_at is detected in the constraints argument.
149
+ # @see Parse::Core::Querying.all
150
+ # @see Parse::Core::Actions.save_all
151
+ def each(constraints = {}, &block)
152
+ # verify we don't hvae created at as a constraint, otherwise this will not work
153
+ invalid_constraints = constraints.keys.any? do |k|
154
+ (k == :created_at || k == :createdAt) ||
155
+ (k.is_a?(Parse::Operation) && (k.operand == :created_at || k.operand == :createdAt))
156
+ end
157
+ if invalid_constraints
158
+ raise ArgumentError, "[#{self.class}.each] Special method each()" \
159
+ "cannot be used with a :created_at constraint."
160
+ end
161
+ batch_size = 250
162
+ start_cursor = first(order: :created_at.asc, keys: :created_at)
163
+ constraints.merge! cache: false, limit: batch_size, order: :created_at.asc
164
+ _all_query = query(constraints) # used for reference in loop below
165
+ cursor = start_cursor
166
+ # the exclusion set is a set of ids not to include the next query.
167
+ exclusion_set = []
168
+ loop do
169
+ _q = query(constraints.dup)
170
+ _q.where(:created_at.on_or_after => cursor.created_at)
171
+ # set of ids not to include in the next query. non-performant, but accurate.
172
+ _q.where(:id.nin => exclusion_set) unless exclusion_set.empty?
173
+ results = _q.results # get results
174
+
175
+ break cursor if results.empty? # break if no results
176
+ results.each(&block)
177
+ next_cursor = results.last
178
+ # break if we got less than the maximum requested
179
+ break next_cursor if results.count < batch_size
180
+ # break if the next object is the same as the current object.
181
+ break next_cursor if cursor.id == next_cursor.id
182
+ # The exclusion set is used in the case where multiple records have the exact
183
+ # same created_at date (down to the microsecond). This prevents getting the same
184
+ # record in the next query request.
185
+ exclusion_set = results.select { |r| r.created_at == next_cursor.created_at }.map(&:id)
186
+ results = nil
187
+ cursor = next_cursor
188
+ end
189
+ end
190
+
191
+ # Fetch all matching objects in this collection matching the constraints.
192
+ # This will be the most common way when querying Parse objects for a subclass.
193
+ # When no block is passed, all objects are returned. Using a block is more memory
194
+ # efficient as matching objects are fetched in batches and discarded after the iteration
195
+ # is completed.
196
+ # @param constraints [Hash] a set of {Parse::Query} constraints.
197
+ # @yield a block to iterate with each matching object.
198
+ # @example
199
+ #
200
+ # songs = Song.all( ... expressions ...) # => array of Parse::Objects
201
+ # # memory efficient for large amounts of records.
202
+ # Song.all( ... expressions ...) do |song|
203
+ # # ... do something with song..
204
+ # end
205
+ #
206
+ # @note This method will continually query for records by automatically
207
+ # incrementing the *:skip* parameter until no more results are returned
208
+ # by the server.
209
+ # @return [Array<Parse::Object>] an array of matching objects. If a block is passed,
210
+ # an empty array is returned.
211
+ def all(constraints = { limit: :max }, &block)
212
+ constraints = constraints.reverse_merge({ limit: :max })
213
+ prepared_query = query(constraints)
214
+ return prepared_query.results(&block) if block_given?
215
+ prepared_query.results
216
+ end
217
+
218
+ # Returns the first item matching the constraint.
219
+ # @overload first(count = 1)
220
+ # @param count [Interger] The number of items to return.
221
+ # @example
222
+ # Object.first(2) # => an array of the first 2 objects in the collection.
223
+ # @return [Parse::Object] if count == 1
224
+ # @return [Array<Parse::Object>] if count > 1
225
+ # @overload first(constraints = {})
226
+ # @param constraints [Hash] a set of {Parse::Query} constraints.
227
+ # @example
228
+ # Object.first( :name => "Anthony" )
229
+ # @return [Parse::Object] the first matching object.
230
+ def first(constraints = {})
231
+ fetch_count = 1
232
+ if constraints.is_a?(Numeric)
233
+ fetch_count = constraints.to_i
234
+ constraints = {}
235
+ end
236
+ constraints.merge!({ limit: fetch_count })
237
+ res = query(constraints).results
238
+ return res.first if fetch_count == 1
239
+ return res.first fetch_count
240
+ end
241
+
242
+ # Returns the most recently created object (ordered by created_at descending).
243
+ # @overload latest(count = 1)
244
+ # @param count [Integer] The number of items to return.
245
+ # @example
246
+ # Object.latest(3) # => an array of the 3 most recently created objects.
247
+ # @return [Parse::Object] if count == 1
248
+ # @return [Array<Parse::Object>] if count > 1
249
+ # @overload latest(constraints = {})
250
+ # @param constraints [Hash] a set of {Parse::Query} constraints.
251
+ # Supports a :limit key to override the default limit of 1.
252
+ # @example
253
+ # Object.latest(category: "news") # => most recent object in news category
254
+ # Object.latest(:user.eq => user, limit: 5) # => 5 most recent for user
255
+ # @return [Parse::Object] the most recently created object matching constraints.
256
+ def latest(constraints = {})
257
+ fetch_count = 1
258
+ if constraints.is_a?(Numeric)
259
+ fetch_count = constraints.to_i
260
+ constraints = {}
261
+ else
262
+ # Allow limit to be specified in constraints hash
263
+ fetch_count = constraints.delete(:limit) || 1
264
+ end
265
+ constraints.merge!({ limit: fetch_count, order: :created_at.desc })
266
+ res = query(constraints).results
267
+ return res.first if fetch_count == 1
268
+ return res.first fetch_count
269
+ end
270
+
271
+ # Returns the most recently updated object (ordered by updated_at descending).
272
+ # @overload last_updated(count = 1)
273
+ # @param count [Integer] The number of items to return.
274
+ # @example
275
+ # Object.last_updated(5) # => an array of the 5 most recently updated objects.
276
+ # @return [Parse::Object] if count == 1
277
+ # @return [Array<Parse::Object>] if count > 1
278
+ # @overload last_updated(constraints = {})
279
+ # @param constraints [Hash] a set of {Parse::Query} constraints.
280
+ # Supports a :limit key to override the default limit of 1.
281
+ # @example
282
+ # Object.last_updated(status: "active") # => most recently updated active object
283
+ # Object.last_updated(:user.eq => user, limit: 3) # => 3 most recently updated for user
284
+ # @return [Parse::Object] the most recently updated object matching constraints.
285
+ def last_updated(constraints = {})
286
+ fetch_count = 1
287
+ if constraints.is_a?(Numeric)
288
+ fetch_count = constraints.to_i
289
+ constraints = {}
290
+ else
291
+ # Allow limit to be specified in constraints hash
292
+ fetch_count = constraints.delete(:limit) || 1
293
+ end
294
+ constraints.merge!({ limit: fetch_count, order: :updated_at.desc })
295
+ res = query(constraints).results
296
+ return res.first if fetch_count == 1
297
+ return res.first fetch_count
298
+ end
299
+
300
+ # Creates a count request which is more performant when counting objects.
301
+ # @example
302
+ # # number of songs with a like count greater than 20.
303
+ # count = Song.count( :like_count.gt => 20 )
304
+ # @param constraints (see #all)
305
+ # @return [Interger] the number of records matching the query.
306
+ # @see Parse::Query#count
307
+ def count(constraints = {})
308
+ query(constraints).count
309
+ end
310
+
311
+ # Counts the number of distinct values for a specified field.
312
+ # Uses MongoDB aggregation pipeline to efficiently count unique values.
313
+ # @example
314
+ # # get count of unique genres for songs with play_count > 100
315
+ # distinct_genres_count = Song.count_distinct(:genre, :play_count.gt => 100)
316
+ # # get total number of unique users
317
+ # unique_users = User.count_distinct(:objectId)
318
+ # @param field [Symbol|String] The name of the field to count distinct values for.
319
+ # @param constraints (see #all)
320
+ # @return [Integer] the number of distinct values
321
+ # @see Parse::Query#count_distinct
322
+ def count_distinct(field, constraints = {})
323
+ query(constraints).count_distinct(field)
324
+ end
325
+
326
+ # Finds the distinct values for a specified field across a single
327
+ # collection or view and returns the results in an array.
328
+ # @example
329
+ # # get a list of unique city names for users who are older than 21.
330
+ # cities = User.distinct(:city, :age.gt => 21 )
331
+ # @param field The name of the field to use for unique aggregation.
332
+ # @param constraints (see #all)
333
+ # @return [Array] a list of distinct values
334
+ # @see Parse::Query#distinct
335
+ def distinct(field, constraints = {})
336
+ query(constraints).distinct(field)
337
+ end
338
+
339
+ # Find objects matching the constraint ordered by the descending created_at date.
340
+ # @param constraints (see #all)
341
+ # @return [Array<Parse::Object>]
342
+ def newest(constraints = {})
343
+ constraints.merge!(order: :created_at.desc)
344
+ _q = query(constraints)
345
+ _q.define_singleton_method(:method_missing) { |m, *args, &block| self.results.send(m, *args, &block) }
346
+ _q
347
+ end
348
+
349
+ # Find objects matching the constraint ordered by the ascending created_at date.
350
+ # @param constraints (see #all)
351
+ # @return [Array<Parse::Object>]
352
+ def oldest(constraints = {})
353
+ constraints.merge!(order: :created_at.asc)
354
+ _q = query(constraints)
355
+ _q.define_singleton_method(:method_missing) { |m, *args, &block| self.results.send(m, *args, &block) }
356
+ _q
357
+ end
358
+
359
+ # Create a cursor-based paginator for efficiently traversing large datasets.
360
+ # This is more efficient than skip/offset pagination for large result sets.
361
+ #
362
+ # @example Basic usage
363
+ # cursor = Song.cursor(limit: 100, order: :created_at.desc)
364
+ # cursor.each_page do |page|
365
+ # process(page)
366
+ # end
367
+ #
368
+ # @example With constraints
369
+ # cursor = Song.cursor(artist: "Artist Name", limit: 50)
370
+ # cursor.each { |song| puts song.title }
371
+ #
372
+ # @param constraints [Hash] query constraints to apply
373
+ # @param limit [Integer] number of items per page (default: 100)
374
+ # @param order [Symbol, Parse::Order] the ordering for pagination
375
+ # @return [Parse::Cursor] a cursor for paginating results
376
+ # @see Parse::Cursor
377
+ def cursor(constraints = {}, limit: 100, order: nil)
378
+ query(constraints).cursor(limit: limit, order: order)
379
+ end
380
+
381
+ # Subscribe to real-time updates for objects in this collection.
382
+ # Uses Parse LiveQuery WebSocket connection to receive push notifications
383
+ # when objects are created, updated, deleted, or enter/leave the query results.
384
+ #
385
+ # @example Basic subscription (all objects)
386
+ # subscription = Song.subscribe
387
+ # subscription.on(:create) { |song| puts "New song: #{song.title}" }
388
+ # subscription.on(:update) { |song, original| puts "Updated!" }
389
+ # subscription.on(:delete) { |song| puts "Deleted!" }
390
+ #
391
+ # @example Subscribe with query constraints
392
+ # subscription = Song.subscribe(where: { artist: "Beatles" })
393
+ # subscription.on_create { |song| puts "New Beatles song!" }
394
+ #
395
+ # @example With field filtering
396
+ # subscription = User.subscribe(where: { status: "online" }, fields: ["name", "avatar"])
397
+ # subscription.on_update { |user| puts "User changed: #{user.name}" }
398
+ #
399
+ # @example With session token for ACL-aware subscriptions
400
+ # subscription = PrivateData.subscribe(session_token: current_user.session_token)
401
+ #
402
+ # @param where [Hash] query constraints for the subscription
403
+ # @param fields [Array<String>] specific fields to watch for changes (nil = all fields)
404
+ # @param session_token [String] session token for ACL-aware subscriptions
405
+ # @param client [Parse::LiveQuery::Client] custom LiveQuery client (optional)
406
+ # @return [Parse::LiveQuery::Subscription] the subscription object
407
+ # @see Parse::LiveQuery::Subscription
408
+ # @see Parse::Query#subscribe
409
+ def subscribe(where: {}, fields: nil, session_token: nil, client: nil)
410
+ query(where).subscribe(fields: fields, session_token: session_token, client: client)
411
+ end
412
+
413
+ # Find objects for a given objectId in this collection. The result is a list
414
+ # (or single item) of the objects that were successfully found.
415
+ # By default, bypasses the cache to ensure fresh data from the server.
416
+ # @example
417
+ # Object.find "<objectId>"
418
+ # Object.find "<objectId>", "<objectId>"....
419
+ # Object.find ["<objectId>", "<objectId>"]
420
+ # Object.find "<objectId>", cache: true # opt-in to cache
421
+ # @param parse_ids [String] the objectId to find.
422
+ # @param type [Symbol] the fetching methodology to use if more than one id was passed.
423
+ # - *:parallel* : Utilizes parrallel HTTP requests to fetch all objects requested.
424
+ # - *:batch* : This uses a batch fetch request using a contained_in clause.
425
+ # @param compact [Boolean] whether to remove nil items from the returned array for objects
426
+ # that were not found.
427
+ # @param cache [Boolean, Symbol] caching mode. Defaults to :write_only when Parse.cache_write_on_fetch is true.
428
+ # - :write_only (default) - skip cache read, but update cache with fresh data
429
+ # - true - read from and write to cache
430
+ # - false - completely bypass cache (no read or write)
431
+ # @return [Parse::Object] if only one id was provided as a parameter.
432
+ # @return [Array<Parse::Object>] if more than one id was provided as a parameter.
433
+ def find(*parse_ids, type: :parallel, compact: true, cache: nil)
434
+ # flatten the list of Object ids.
435
+ parse_ids.flatten!
436
+ parse_ids.compact!
437
+ # determines if the result back to the call site is an array or a single result
438
+ as_array = parse_ids.count > 1
439
+ results = []
440
+
441
+ # Default to write-only cache mode - find always gets fresh data
442
+ # but updates cache for future cached reads. Controlled by feature flag.
443
+ if cache.nil?
444
+ cache = Parse.cache_write_on_fetch ? :write_only : false
445
+ end
446
+
447
+ # Extract cache option for client requests
448
+ client_opts = { cache: cache }
449
+
450
+ if type == :batch
451
+ # use a .in query with the given id as a list
452
+ query = self.class.query(:id.in => parse_ids)
453
+ query.cache = cache
454
+ results = query.results
455
+ else
456
+ # use Parallel to make multiple threaded requests for finding these objects.
457
+ # The benefit of using this as default is that each request goes to a specific URL
458
+ # which is better than Query request (table scan). This in turn allows for caching of
459
+ # individual objects.
460
+ results = parse_ids.threaded_map do |parse_id|
461
+ next nil unless parse_id.present?
462
+ response = client.fetch_object(parse_class, parse_id, **client_opts)
463
+ next nil if response.error?
464
+ Parse::Object.build response.result, parse_class
465
+ end
466
+ end
467
+ # removes any nil items in the array
468
+ results.compact! if compact
469
+
470
+ as_array ? results : results.first
471
+ end
472
+
473
+ alias_method :get, :find
474
+
475
+ # Find objects with caching enabled. This is a convenience method that calls
476
+ # find with cache: true.
477
+ # @example
478
+ # Object.find_cached "<objectId>"
479
+ # Object.find_cached "<objectId>", "<objectId>"....
480
+ # @param parse_ids [String] the objectId(s) to find.
481
+ # @param type [Symbol] the fetching methodology (:parallel or :batch).
482
+ # @param compact [Boolean] whether to remove nil items from the returned array.
483
+ # @return [Parse::Object] if only one id was provided as a parameter.
484
+ # @return [Array<Parse::Object>] if more than one id was provided as a parameter.
485
+ # @see #find
486
+ def find_cached(*parse_ids, type: :parallel, compact: true)
487
+ find(*parse_ids, type: type, compact: compact, cache: true)
488
+ end
489
+ end # Querying
490
+ end
491
+ end
@@ -0,0 +1,202 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "properties"
5
+
6
+ module Parse
7
+ module Core
8
+ # Defines the Schema methods applied to a Parse::Object.
9
+ module Schema
10
+
11
+ # Generate a Parse-server compatible schema hash for performing changes to the
12
+ # structure of the remote collection.
13
+ # @return [Hash] the schema for this Parse::Object subclass.
14
+ def schema
15
+ sch = { className: parse_class, fields: {} }
16
+ #first go through all the attributes
17
+ attributes.each do |k, v|
18
+ # don't include the base Parse fields
19
+ next if Parse::Properties::BASE.include?(k)
20
+ next if v.nil?
21
+ result = { type: v.to_s.camelize }
22
+ # if it is a basic column property, find the right datatype
23
+ case v
24
+ when :integer, :float
25
+ result[:type] = Parse::Model::TYPE_NUMBER
26
+ when :geopoint, :geo_point
27
+ result[:type] = Parse::Model::TYPE_GEOPOINT
28
+ when :polygon, :geo_polygon
29
+ result[:type] = Parse::Model::TYPE_POLYGON
30
+ when :pointer
31
+ result = { type: Parse::Model::TYPE_POINTER, targetClass: references[k] }
32
+ when :acl
33
+ result[:type] = Parse::Model::ACL
34
+ when :timezone, :time_zone
35
+ result[:type] = "String" # no TimeZone native in Parse
36
+ else
37
+ result[:type] = v.to_s.camelize
38
+ end
39
+
40
+ sch[:fields][k] = result
41
+ end
42
+ #then add all the relational column attributes
43
+ relations.each do |k, v|
44
+ sch[:fields][k] = { type: Parse::Model::TYPE_RELATION, targetClass: relations[k] }
45
+ end
46
+ sch
47
+ end
48
+
49
+ # Update the remote schema for this Parse collection.
50
+ # @param schema_updates [Hash] the changes to be made to the schema.
51
+ # @return [Parse::Response]
52
+ def update_schema(schema_updates = nil)
53
+ schema_updates ||= schema
54
+ client.update_schema parse_class, schema_updates
55
+ end
56
+
57
+ # Create a new collection for this model with the schema defined by the local
58
+ # model.
59
+ # @return [Parse::Response]
60
+ # @see Schema.schema
61
+ def create_schema
62
+ client.create_schema parse_class, schema
63
+ end
64
+
65
+ # Fetche the current schema for this collection from Parse server.
66
+ # @return [Parse::Response]
67
+ def fetch_schema
68
+ client.schema parse_class
69
+ end
70
+
71
+ # System classes that cannot be created or modified via the schema API.
72
+ # These are managed automatically by Parse Server.
73
+ SCHEMA_READONLY_CLASSES = [
74
+ Parse::Model::CLASS_PUSH_STATUS,
75
+ Parse::Model::CLASS_SCHEMA
76
+ ].freeze
77
+
78
+ # Default CLP that grants public access to all operations.
79
+ # Used to reset CLPs before applying new ones.
80
+ DEFAULT_PUBLIC_CLP = {
81
+ "find" => { "*" => true },
82
+ "get" => { "*" => true },
83
+ "count" => { "*" => true },
84
+ "create" => { "*" => true },
85
+ "update" => { "*" => true },
86
+ "delete" => { "*" => true },
87
+ "addField" => { "*" => true }
88
+ }.freeze
89
+
90
+ # Reset the CLP on the server to public defaults.
91
+ # This clears any existing restrictive permissions.
92
+ #
93
+ # @param client [Parse::Client] optional client to use
94
+ # @return [Parse::Response] the response from the server
95
+ #
96
+ # @example Reset CLPs to public
97
+ # Song.reset_clp!
98
+ def reset_clp!(client: nil)
99
+ client ||= self.client
100
+
101
+ unless client.master_key.present?
102
+ warn "[Parse] CLP reset for #{parse_class} requires the master key!"
103
+ return nil
104
+ end
105
+
106
+ client.update_schema(parse_class, { "classLevelPermissions" => DEFAULT_PUBLIC_CLP })
107
+ end
108
+
109
+ # A class method for non-destructive auto upgrading a remote schema based
110
+ # on the properties and relations you have defined in your local model. If
111
+ # the collection doesn't exist, we create the schema. If the collection already
112
+ # exists, the current schema is fetched, and only add the additional fields
113
+ # that are missing.
114
+ #
115
+ # Also updates Class-Level Permissions (CLPs) if defined on the model using
116
+ # the `set_clp` and `protect_fields` DSL methods.
117
+ #
118
+ # @note This feature requires use of the master_key. No columns or fields are removed, this is a safe non-destructive upgrade.
119
+ # @param include_clp [Boolean] whether to also update CLPs (default: true)
120
+ # @return [Parse::Response] if the remote schema was modified.
121
+ # @return [Boolean] if no changes were made to the schema, it returns true.
122
+ def auto_upgrade!(include_clp: true)
123
+ # Skip read-only system classes that Parse Server manages automatically
124
+ if SCHEMA_READONLY_CLASSES.include?(parse_class)
125
+ warn "[Parse] Skipping #{parse_class} - managed automatically by Parse Server"
126
+ return true
127
+ end
128
+
129
+ unless client.master_key.present?
130
+ warn "[Parse] Schema changes for #{parse_class} is only available with the master key!"
131
+ return false
132
+ end
133
+ # fetch the current schema (requires master key)
134
+ response = fetch_schema
135
+
136
+ # if it's a core class that doesn't exist, then create the collection without any fields,
137
+ # since parse-server will automatically create the collection with the set of core fields.
138
+ # then fetch the schema again, to add the missing fields.
139
+ if response.error? && self.to_s.start_with?("Parse::") #is it a core class?
140
+ client.create_schema parse_class, {}
141
+ response = fetch_schema
142
+ # if it still wasn't able to be created, raise an error.
143
+ if response.error?
144
+ warn "[Parse] Schema error: unable to create class #{parse_class}"
145
+ return response
146
+ end
147
+ end
148
+
149
+ if response.success?
150
+ #let's figure out the diff fields
151
+ remote_fields = response.result["fields"]
152
+ current_schema = schema
153
+ current_schema[:fields] = current_schema[:fields].reduce({}) do |h, (k, v)|
154
+ #if the field does not exist in Parse, then add it to the update list
155
+ h[k] = v if remote_fields[k.to_s].nil?
156
+ h
157
+ end
158
+
159
+ # Handle CLP updates if configured and requested
160
+ if include_clp && respond_to?(:class_permissions) && class_permissions.present?
161
+ # First, reset CLPs to public defaults to clear any old restrictive permissions.
162
+ # Parse Server merges CLPs rather than replacing them, so old keys can persist
163
+ # and cause "Permission denied" errors if not explicitly cleared.
164
+ reset_clp!
165
+
166
+ # Now apply the new CLP configuration
167
+ current_schema[:classLevelPermissions] = class_permissions.as_json(include_defaults: true)
168
+ elsif include_clp && _default_class_level_permissions_for_upgrade
169
+ # Opt-in global default: applies only on the initial create path
170
+ # below (existing classes do NOT get rewritten here — we only
171
+ # reach this branch when current_schema[:fields] is non-empty,
172
+ # i.e. there are NEW columns to add; we still avoid mutating
173
+ # existing CLPs to prevent surprising lockouts).
174
+ end
175
+
176
+ return true if current_schema[:fields].empty? && !current_schema[:classLevelPermissions]
177
+ return update_schema(current_schema)
178
+ end
179
+
180
+ # Create new schema (class doesn't exist)
181
+ initial_schema = schema
182
+ # Include CLPs in initial schema creation if configured. Order of
183
+ # precedence:
184
+ # 1. Per-model `class_permissions` (DSL)
185
+ # 2. Global `Parse::Schema.default_class_level_permissions` opt-in
186
+ # 3. None — Parse Server's wide-open defaults apply
187
+ if include_clp && respond_to?(:class_permissions) && class_permissions.present?
188
+ initial_schema[:classLevelPermissions] = class_permissions.as_json(include_defaults: true)
189
+ elsif include_clp && (default_clps = _default_class_level_permissions_for_upgrade)
190
+ initial_schema[:classLevelPermissions] = default_clps
191
+ end
192
+ client.create_schema parse_class, initial_schema
193
+ end
194
+
195
+ # @api private
196
+ def _default_class_level_permissions_for_upgrade
197
+ return nil unless defined?(Parse::Schema)
198
+ Parse::Schema.default_class_level_permissions
199
+ end
200
+ end
201
+ end
202
+ end