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,262 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ # Note: Do not require "../object" here - this file is loaded from object.rb
5
+ # and adding that require would create a circular dependency.
6
+
7
+ module Parse
8
+ # This class represents the data and columns contained in the standard Parse
9
+ # `_Audience` collection. Audiences are pre-defined groups of installations
10
+ # that can be targeted for push notifications. They store query constraints
11
+ # that define which installations belong to the audience.
12
+ #
13
+ # Audiences are useful for:
14
+ # - Reusable push targets (e.g., "VIP Users", "Beta Testers")
15
+ # - A/B testing different user segments
16
+ # - Marketing campaigns to specific demographics
17
+ #
18
+ # == Caching
19
+ #
20
+ # Audience queries are cached by default to improve push notification performance.
21
+ # The cache has a configurable TTL (default: 5 minutes).
22
+ #
23
+ # @example Configure cache TTL
24
+ # Parse::Audience.cache_ttl = 600 # 10 minutes
25
+ #
26
+ # @example Clear the cache
27
+ # Parse::Audience.clear_cache!
28
+ #
29
+ # @example Bypass cache for a specific lookup
30
+ # audience = Parse::Audience.find_by_name("VIP Users", cache: false)
31
+ #
32
+ # The default schema for the {Audience} class is as follows:
33
+ # class Parse::Audience < Parse::Object
34
+ # # See Parse::Object for inherited properties...
35
+ #
36
+ # property :name
37
+ # property :query, :object # The Installation query constraints
38
+ # end
39
+ #
40
+ # @example Creating an audience
41
+ # audience = Parse::Audience.new(
42
+ # name: "iOS VIP Users",
43
+ # query: { "deviceType" => "ios", "vip" => true }
44
+ # )
45
+ # audience.save
46
+ #
47
+ # @example Targeting an audience with push
48
+ # Parse::Push.new
49
+ # .to_audience("iOS VIP Users")
50
+ # .with_alert("Exclusive offer!")
51
+ # .send!
52
+ #
53
+ # @see Parse::Push#to_audience
54
+ # @see Parse::Object
55
+ class Audience < Parse::Object
56
+ parse_class Parse::Model::CLASS_AUDIENCE
57
+
58
+ # Default cache TTL in seconds (5 minutes)
59
+ DEFAULT_CACHE_TTL = 300
60
+
61
+ class << self
62
+ # @return [Integer] the cache TTL in seconds (default: 300)
63
+ attr_writer :cache_ttl
64
+
65
+ def cache_ttl
66
+ @cache_ttl ||= DEFAULT_CACHE_TTL
67
+ end
68
+
69
+ # Clear the audience cache
70
+ # @return [void]
71
+ def clear_cache!
72
+ cache_mutex.synchronize do
73
+ @audience_cache = {}
74
+ @cache_timestamps = {}
75
+ end
76
+ end
77
+
78
+ # Get an audience from cache or fetch from server
79
+ # @param name [String] the audience name
80
+ # @param cache [Boolean] whether to use cache (default: true)
81
+ # @return [Parse::Audience, nil] the audience or nil if not found
82
+ def cache_fetch(name, cache: true)
83
+ return find_by_name_uncached(name) unless cache
84
+
85
+ cache_mutex.synchronize do
86
+ @audience_cache ||= {}
87
+ @cache_timestamps ||= {}
88
+
89
+ # Cleanup expired entries periodically to prevent memory growth
90
+ cleanup_expired_cache_entries
91
+
92
+ cached = @audience_cache[name]
93
+ timestamp = @cache_timestamps[name]
94
+
95
+ # Check if cache is valid
96
+ if timestamp && (Time.now.to_i - timestamp) < cache_ttl
97
+ return cached
98
+ end
99
+
100
+ # Fetch and cache (fetch happens inside lock - acceptable for short TTL cache)
101
+ audience = find_by_name_uncached(name)
102
+ @audience_cache[name] = audience
103
+ @cache_timestamps[name] = Time.now.to_i
104
+
105
+ audience
106
+ end
107
+ end
108
+
109
+ # Remove expired entries from cache to prevent memory leaks
110
+ # Called automatically during cache_fetch, but can also be called manually
111
+ # @return [Integer] number of entries removed
112
+ def cleanup_expired_cache!
113
+ cache_mutex.synchronize do
114
+ cleanup_expired_cache_entries
115
+ end
116
+ end
117
+
118
+ # Thread-safe mutex for cache operations
119
+ # @return [Mutex]
120
+ def cache_mutex
121
+ @cache_mutex ||= Mutex.new
122
+ end
123
+
124
+ private
125
+
126
+ # Internal method to cleanup expired cache entries (must be called within synchronize block)
127
+ # @return [Integer] number of entries removed
128
+ def cleanup_expired_cache_entries
129
+ return 0 unless @cache_timestamps
130
+
131
+ now = Time.now.to_i
132
+ expired_keys = @cache_timestamps.select { |_key, ts| now - ts >= cache_ttl }.keys
133
+
134
+ expired_keys.each do |key|
135
+ @audience_cache&.delete(key)
136
+ @cache_timestamps.delete(key)
137
+ end
138
+
139
+ expired_keys.size
140
+ end
141
+
142
+ def find_by_name_uncached(name)
143
+ first(name: name)
144
+ end
145
+ end
146
+
147
+ # @!attribute name
148
+ # The display name of this audience.
149
+ # @return [String] The audience name.
150
+ property :name
151
+
152
+ # @!attribute query
153
+ # The query constraints that define which installations belong to this audience.
154
+ # This is stored as a hash matching the Installation query format.
155
+ # @return [Hash] The query constraint hash.
156
+ # @example
157
+ # audience.query = { "deviceType" => "ios", "appVersion" => { "$gte" => "2.0" } }
158
+ property :query, :object
159
+
160
+ # Alias for query to match Parse Server naming conventions.
161
+ # @return [Hash] The query constraint hash.
162
+ def query_constraint
163
+ query
164
+ end
165
+
166
+ # Set the query constraint.
167
+ # @param constraints [Hash] The query constraint hash.
168
+ def query_constraint=(constraints)
169
+ self.query = constraints
170
+ end
171
+
172
+ class << self
173
+ # Find an audience by name (uses cache by default).
174
+ # @param name [String] the audience name
175
+ # @param cache [Boolean] whether to use cache (default: true)
176
+ # @return [Parse::Audience, nil] the audience or nil if not found
177
+ # @example
178
+ # audience = Parse::Audience.find_by_name("VIP Users")
179
+ # audience = Parse::Audience.find_by_name("VIP Users", cache: false) # Bypass cache
180
+ def find_by_name(name, cache: true)
181
+ cache_fetch(name, cache: cache)
182
+ end
183
+
184
+ # Get the count of installations matching an audience's query.
185
+ # @param audience_name [String] the audience name
186
+ # @return [Integer] the count of matching installations
187
+ # @raise [Parse::Push::AudienceNotFound] if no audience exists with the
188
+ # given name. Previously returned 0 on miss, which was indistinguishable
189
+ # from "audience exists but matches nothing."
190
+ # @example
191
+ # count = Parse::Audience.installation_count("VIP Users")
192
+ def installation_count(audience_name)
193
+ audience = find_by_name(audience_name)
194
+ if audience.nil?
195
+ raise Parse::Push::AudienceNotFound,
196
+ "Audience '#{audience_name}' not found in _Audience collection"
197
+ end
198
+ return 0 unless audience.query.present?
199
+
200
+ q = Parse::Installation.query
201
+ audience.query.each do |key, value|
202
+ q.where(key.to_sym => value)
203
+ end
204
+ q.count
205
+ end
206
+
207
+ # Get a query for installations matching an audience.
208
+ # @param audience_name [String] the audience name
209
+ # @return [Parse::Query] a query for matching installations
210
+ # @raise [Parse::Push::AudienceNotFound] if no audience exists with the
211
+ # given name. Previously returned an unconstrained Installation query
212
+ # on miss, which silently elevated the result set from "matches this
213
+ # audience" to "every Installation" — the same fail-open footgun as
214
+ # {Parse::Push#to_audience}.
215
+ # @example
216
+ # installations = Parse::Audience.installations("VIP Users").all
217
+ def installations(audience_name)
218
+ audience = find_by_name(audience_name)
219
+ if audience.nil?
220
+ raise Parse::Push::AudienceNotFound,
221
+ "Audience '#{audience_name}' not found in _Audience collection"
222
+ end
223
+ q = Parse::Installation.query
224
+ if audience.query.present?
225
+ audience.query.each do |key, value|
226
+ q.where(key.to_sym => value)
227
+ end
228
+ end
229
+ q
230
+ end
231
+ end
232
+
233
+ # Get the count of installations matching this audience's query.
234
+ # @return [Integer] the count of matching installations
235
+ # @example
236
+ # audience = Parse::Audience.first
237
+ # puts "#{audience.name} has #{audience.installation_count} members"
238
+ def installation_count
239
+ return 0 unless query.present?
240
+
241
+ q = Parse::Installation.query
242
+ query.each do |key, value|
243
+ q.where(key.to_sym => value)
244
+ end
245
+ q.count
246
+ end
247
+
248
+ # Get a query for installations matching this audience.
249
+ # @return [Parse::Query] a query for matching installations
250
+ # @example
251
+ # audience.installations.each { |i| puts i.device_token }
252
+ def installations
253
+ q = Parse::Installation.query
254
+ if query.present?
255
+ query.each do |key, value|
256
+ q.where(key.to_sym => value)
257
+ end
258
+ end
259
+ q
260
+ end
261
+ end
262
+ end
@@ -0,0 +1,363 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+ # Note: Do not require "../object" here - this file is loaded from object.rb
4
+ # and adding that require would create a circular dependency.
5
+
6
+ module Parse
7
+ # This class represents the data and columns contained in the standard Parse
8
+ # `_Installation` collection. This class is also responsible for managing the
9
+ # device tokens for mobile devices in order to use push notifications. All queries done
10
+ # to send pushes using Parse::Push are performed against the Installation collection.
11
+ # An installation object represents an instance of your app being installed
12
+ # on a device. These objects are used to store subscription data for
13
+ # installations which have subscribed to one or more push notification channels.
14
+ #
15
+ # The default schema for {Installation} is as follows:
16
+ #
17
+ # class Parse::Installation < Parse::Object
18
+ # # See Parse::Object for inherited properties...
19
+ #
20
+ # property :gcm_sender_id, field: :GCMSenderId
21
+ # property :app_identifier
22
+ # property :app_name
23
+ # property :app_version
24
+ # property :app_build_number
25
+ # property :badge, :integer
26
+ # property :channels, :array
27
+ # property :device_token
28
+ # property :device_token_last_modified, :integer
29
+ # property :device_type, enum: [:ios, :android, :osx, :tvos, :watchos, :web, :expo, :win, :other, :unknown, :unsupported]
30
+ # property :installation_id
31
+ # property :locale_identifier
32
+ # property :parse_version
33
+ # property :push_type
34
+ # property :time_zone, :timezone
35
+ #
36
+ # has_one :session, ->{ where(installation_id: i.installation_id) }, scope_only: true
37
+ # end
38
+ # @see Push
39
+ # @see Parse::Object
40
+ class Installation < Parse::Object
41
+ parse_class Parse::Model::CLASS_INSTALLATION
42
+ # @!attribute gcm_sender_id
43
+ # This field only has meaning for Android installations that use the GCM
44
+ # push type. It is reserved for directing Parse to send pushes to this
45
+ # installation with an alternate GCM sender ID. This field should generally
46
+ # not be set unless you are uploading installation data from another push
47
+ # provider. If you set this field, then you must set the GCM API key
48
+ # corresponding to this GCM sender ID in your Parse application’s push settings.
49
+ # @return [String]
50
+ property :gcm_sender_id, field: :GCMSenderId
51
+
52
+ # @!attribute app_identifier
53
+ # A unique identifier for this installation’s client application. In iOS, this is the Bundle Identifier.
54
+ # @return [String]
55
+ property :app_identifier
56
+
57
+ # @!attribute app_name
58
+ # The display name of the client application to which this installation belongs.
59
+ # @return [String]
60
+ property :app_name
61
+
62
+ # @!attribute app_version
63
+ # The version string of the client application to which this installation belongs.
64
+ # @return [String]
65
+ property :app_version
66
+
67
+ # @!attribute app_build_number
68
+ # The build number of the client application to which this installation belongs.
69
+ # @return [String]
70
+ property :app_build_number
71
+
72
+ # @!attribute badge
73
+ # A number field representing the last known application badge for iOS installations.
74
+ # @return [Integer]
75
+ property :badge, :integer
76
+
77
+ # @!attribute channels
78
+ # An array of the channels to which a device is currently subscribed.
79
+ # Note that **channelUris** (the Microsoft-generated push URIs for Windows devices) is
80
+ # not supported at this time.
81
+ # @return [Array]
82
+ property :channels, :array
83
+
84
+ # @!attribute device_token
85
+ # The Apple or Google generated token used to deliver messages to the APNs
86
+ # or GCM push networks respectively.
87
+ # @return [String]
88
+ property :device_token
89
+
90
+ # @!attribute device_token_last_modified
91
+ # @return [Integer] number of seconds since token modified
92
+ property :device_token_last_modified, :integer
93
+
94
+ # @!attribute device_type
95
+ # The type of device: "ios", "android", "osx", "tvos", "watchos", "web", "expo", "win",
96
+ # "other", "unknown", or "unsupported".
97
+ # This property is implemented as a Parse::Stack enumeration.
98
+ # @return [String]
99
+ property :device_type, enum: [:ios, :android, :osx, :tvos, :watchos, :web, :expo, :win, :other, :unknown, :unsupported]
100
+
101
+ # @!attribute installation_id
102
+ # Universally Unique Identifier (UUID) for the device used by Parse. It
103
+ # must be unique across all of an app’s installations. (readonly).
104
+ # @return [String]
105
+ property :installation_id
106
+
107
+ # @!attribute locale_identifier
108
+ # The locale for this device.
109
+ # @return [String]
110
+ property :locale_identifier
111
+
112
+ # @!attribute parse_version
113
+ # The version of the Parse SDK which this installation uses.
114
+ # @return [String]
115
+ property :parse_version
116
+
117
+ # @!attribute push_type
118
+ # This field is reserved for directing Parse to the push delivery network
119
+ # to be used. If the device is registered to receive pushes via GCM, this
120
+ # field will be marked “gcm”. If this device is not using GCM, and is
121
+ # using Parse’s push notification service, it will be blank (readonly).
122
+ # @return [String]
123
+ property :push_type
124
+
125
+ # @!attribute time_zone
126
+ # The current time zone where the target device is located. This should be an IANA time zone identifier
127
+ # or a {Parse::TimeZone} instance.
128
+ # @return [Parse::TimeZone]
129
+ property :time_zone, :timezone
130
+
131
+ # @!attribute session
132
+ # Returns the corresponding {Parse::Session} associated with this installation, if any exists.
133
+ # This is implemented as a has_one association to the Session class using the {installation_id}.
134
+ # @version 1.7.1
135
+ # @return [Parse::Session] The associated {Parse::Session} that might be tied to this installation
136
+ has_one :session, -> { where(installation_id: i.installation_id) }, scope_only: true
137
+
138
+ # =========================================================================
139
+ # Channel Management - Class Methods
140
+ # =========================================================================
141
+
142
+ class << self
143
+ # List all unique channel names across all installations.
144
+ # @return [Array<String>] array of channel names
145
+ # @example
146
+ # all_channels = Parse::Installation.all_channels
147
+ # # => ["news", "sports", "weather"]
148
+ def all_channels
149
+ distinct(:channels)
150
+ end
151
+
152
+ # Count the number of installations subscribed to a specific channel.
153
+ # @param channel [String] the channel name to count subscribers for
154
+ # @return [Integer] the number of subscribers
155
+ # @example
156
+ # count = Parse::Installation.subscribers_count("news")
157
+ # # => 1250
158
+ def subscribers_count(channel)
159
+ query(:channels.in => [channel]).count
160
+ end
161
+
162
+ # Get a query for installations subscribed to a specific channel.
163
+ # @param channel [String] the channel name to find subscribers for
164
+ # @return [Parse::Query] a query scoped to the channel's subscribers
165
+ # @example
166
+ # # Get all iOS subscribers to the "news" channel
167
+ # installations = Parse::Installation.subscribers("news")
168
+ # .where(device_type: "ios")
169
+ # .all
170
+ def subscribers(channel)
171
+ query(:channels.in => [channel])
172
+ end
173
+
174
+ # =========================================================================
175
+ # Device Type Scopes
176
+ # =========================================================================
177
+ # Note: ios and android scopes are automatically created by the enum property:
178
+ # property :device_type, enum: [:ios, :android, :osx, :tvos, :watchos, :web, :expo, :win, :other, :unknown, :unsupported]
179
+ # This creates: Installation.ios, Installation.android, etc.
180
+
181
+ # Query scope for a specific device type.
182
+ # @param type [String, Symbol] the device type (ios, android, osx, tvos, watchos, web, expo, win, other, unknown, unsupported)
183
+ # @return [Parse::Query] a query for the specified device type
184
+ # @example
185
+ # mac_devices = Parse::Installation.by_device_type(:osx).all
186
+ def by_device_type(type)
187
+ query(device_type: type.to_s)
188
+ end
189
+
190
+ # =========================================================================
191
+ # Badge Management
192
+ # =========================================================================
193
+
194
+ # Reset badge count for all installations in a channel.
195
+ # @param channel [String] the channel name
196
+ # @return [Integer] the number of installations updated
197
+ # @example
198
+ # Parse::Installation.reset_badges_for_channel("news")
199
+ def reset_badges_for_channel(channel)
200
+ installations = subscribers(channel).where(:badge.gt => 0).all
201
+ installations.each do |installation|
202
+ installation.badge = 0
203
+ installation.save
204
+ end
205
+ installations.count
206
+ end
207
+
208
+ # Reset badge count for all installations of a specific device type.
209
+ # @param type [String, Symbol] the device type (default: :ios since badges are primarily iOS)
210
+ # @return [Integer] the number of installations updated
211
+ # @example
212
+ # Parse::Installation.reset_all_badges
213
+ # Parse::Installation.reset_all_badges(:android)
214
+ def reset_all_badges(type = :ios)
215
+ installations = by_device_type(type).where(:badge.gt => 0).all
216
+ installations.each do |installation|
217
+ installation.badge = 0
218
+ installation.save
219
+ end
220
+ installations.count
221
+ end
222
+
223
+ # =========================================================================
224
+ # Stale Token Detection
225
+ # =========================================================================
226
+
227
+ # Query for installations with stale (old) device tokens.
228
+ # Useful for cleaning up installations that are likely no longer active.
229
+ # @param days [Integer] number of days since last token modification (default: 90)
230
+ # @return [Parse::Query] a query for installations with old tokens
231
+ # @example
232
+ # # Find installations not updated in 90 days
233
+ # stale = Parse::Installation.stale_tokens.all
234
+ #
235
+ # # Find installations not updated in 30 days
236
+ # stale = Parse::Installation.stale_tokens(days: 30).all
237
+ def stale_tokens(days: 90)
238
+ cutoff = Time.now - (days * 24 * 60 * 60)
239
+ query(:updated_at.lt => cutoff)
240
+ end
241
+
242
+ # Count installations with stale tokens.
243
+ # @param days [Integer] number of days since last update (default: 90)
244
+ # @return [Integer] count of stale installations
245
+ # @example
246
+ # count = Parse::Installation.stale_count(days: 60)
247
+ def stale_count(days: 90)
248
+ stale_tokens(days: days).count
249
+ end
250
+
251
+ # Delete all installations with stale tokens.
252
+ # Use with caution - this permanently removes installation records.
253
+ # @param days [Integer] number of days since last update (default: 90)
254
+ # @return [Integer] the number of installations deleted
255
+ # @example
256
+ # # Clean up installations not updated in 180 days
257
+ # deleted = Parse::Installation.cleanup_stale_tokens!(days: 180)
258
+ def cleanup_stale_tokens!(days: 90)
259
+ installations = stale_tokens(days: days).all
260
+ installations.each(&:destroy)
261
+ installations.count
262
+ end
263
+ end
264
+
265
+ # =========================================================================
266
+ # Channel Management - Instance Methods
267
+ # =========================================================================
268
+
269
+ # Subscribe this installation to one or more channels.
270
+ # The changes are automatically saved to the server.
271
+ # @param channel_names [Array<String>] the channel names to subscribe to
272
+ # @return [Boolean] true if the save was successful
273
+ # @example
274
+ # installation.subscribe("news", "weather")
275
+ # installation.subscribe(["sports", "updates"])
276
+ def subscribe(*channel_names)
277
+ self.channels ||= []
278
+ self.channels = (self.channels + channel_names.flatten.map(&:to_s)).uniq
279
+ save
280
+ end
281
+
282
+ # Unsubscribe this installation from one or more channels.
283
+ # The changes are automatically saved to the server.
284
+ # @param channel_names [Array<String>] the channel names to unsubscribe from
285
+ # @return [Boolean] true if the save was successful, or true if no channels were set
286
+ # @example
287
+ # installation.unsubscribe("news")
288
+ # installation.unsubscribe("sports", "weather")
289
+ def unsubscribe(*channel_names)
290
+ return true unless channels.present?
291
+ self.channels = channels - channel_names.flatten.map(&:to_s)
292
+ save
293
+ end
294
+
295
+ # Check if this installation is subscribed to a specific channel.
296
+ # @param channel [String] the channel name to check
297
+ # @return [Boolean] true if subscribed to the channel
298
+ # @example
299
+ # if installation.subscribed_to?("news")
300
+ # puts "Subscribed to news!"
301
+ # end
302
+ def subscribed_to?(channel)
303
+ channels&.include?(channel.to_s) || false
304
+ end
305
+
306
+ # =========================================================================
307
+ # Badge Management - Instance Methods
308
+ # =========================================================================
309
+
310
+ # Reset the badge count to 0 and save.
311
+ # @return [Boolean] true if save was successful
312
+ # @example
313
+ # installation.reset_badge!
314
+ def reset_badge!
315
+ self.badge = 0
316
+ save
317
+ end
318
+
319
+ # Increment the badge count and save.
320
+ # @param amount [Integer] amount to increment by (default: 1)
321
+ # @return [Boolean] true if save was successful
322
+ # @example
323
+ # installation.increment_badge!
324
+ # installation.increment_badge!(5)
325
+ def increment_badge!(amount = 1)
326
+ self.badge = (badge || 0) + amount
327
+ save
328
+ end
329
+
330
+ # =========================================================================
331
+ # Stale Token Detection - Instance Methods
332
+ # =========================================================================
333
+
334
+ # Check if this installation's token is considered stale.
335
+ # @param days [Integer] number of days to consider stale (default: 90)
336
+ # @return [Boolean] true if the installation hasn't been updated in the given days
337
+ # @example
338
+ # if installation.stale?
339
+ # puts "This installation may no longer be active"
340
+ # end
341
+ def stale?(days: 90)
342
+ return false if updated_at.nil?
343
+ cutoff = Time.now - (days * 24 * 60 * 60)
344
+ updated_at < cutoff
345
+ end
346
+
347
+ # Get the number of days since this installation was last updated.
348
+ # @return [Integer, nil] days since last update, or nil if no updated_at
349
+ # @example
350
+ # puts "Last active #{installation.days_since_update} days ago"
351
+ def days_since_update
352
+ return nil if updated_at.nil?
353
+ ((Time.now - updated_at.to_time) / (24 * 60 * 60)).to_i
354
+ end
355
+
356
+ # =========================================================================
357
+ # Device Type Helpers - Instance Methods
358
+ # =========================================================================
359
+ # Note: ios? and android? predicates are automatically created by the enum property:
360
+ # property :device_type, enum: [:ios, :android, :osx, :tvos, :watchos, :web, :expo, :win, :other, :unknown, :unsupported]
361
+ # This creates: installation.ios?, installation.android?, etc.
362
+ end
363
+ end