parse-stack-next 4.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.bundle/config +2 -0
- data/.env.sample +112 -0
- data/.env.test +10 -0
- data/.github/workflows/ruby.yml +36 -0
- data/.gitignore +49 -0
- data/.ruby-version +1 -0
- data/.solargraph.yml +22 -0
- data/CHANGELOG.md +5816 -0
- data/Gemfile +30 -0
- data/Gemfile.lock +175 -0
- data/LICENSE.txt +23 -0
- data/Makefile +63 -0
- data/README.md +5655 -0
- data/Rakefile +573 -0
- data/bin/console +38 -0
- data/bin/parse-console +136 -0
- data/bin/server +17 -0
- data/bin/setup +7 -0
- data/config/parse-config.json +12 -0
- data/docs/TEST_SERVER.md +271 -0
- data/docs/_config.yml +1 -0
- data/docs/mcp_guide.md +3484 -0
- data/docs/mongodb_direct_guide.md +1348 -0
- data/docs/mongodb_index_optimization_guide.md +631 -0
- data/examples/transaction_example.rb +219 -0
- data/lib/parse/acl_scope.rb +728 -0
- data/lib/parse/agent/cancellation_token.rb +80 -0
- data/lib/parse/agent/constraint_translator.rb +480 -0
- data/lib/parse/agent/describe.rb +420 -0
- data/lib/parse/agent/errors.rb +133 -0
- data/lib/parse/agent/mcp_client.rb +557 -0
- data/lib/parse/agent/mcp_dispatcher.rb +1023 -0
- data/lib/parse/agent/mcp_rack_app.rb +1143 -0
- data/lib/parse/agent/mcp_server.rb +376 -0
- data/lib/parse/agent/metadata_audit.rb +259 -0
- data/lib/parse/agent/metadata_dsl.rb +733 -0
- data/lib/parse/agent/metadata_registry.rb +794 -0
- data/lib/parse/agent/pipeline_validator.rb +82 -0
- data/lib/parse/agent/prompts.rb +351 -0
- data/lib/parse/agent/rate_limiter.rb +158 -0
- data/lib/parse/agent/relation_graph.rb +162 -0
- data/lib/parse/agent/result_formatter.rb +453 -0
- data/lib/parse/agent/tools.rb +5489 -0
- data/lib/parse/agent.rb +3249 -0
- data/lib/parse/api/aggregate.rb +79 -0
- data/lib/parse/api/all.rb +26 -0
- data/lib/parse/api/analytics.rb +18 -0
- data/lib/parse/api/batch.rb +33 -0
- data/lib/parse/api/cloud_functions.rb +58 -0
- data/lib/parse/api/config.rb +125 -0
- data/lib/parse/api/files.rb +29 -0
- data/lib/parse/api/hooks.rb +117 -0
- data/lib/parse/api/objects.rb +146 -0
- data/lib/parse/api/path_segment.rb +75 -0
- data/lib/parse/api/push.rb +20 -0
- data/lib/parse/api/schema.rb +49 -0
- data/lib/parse/api/server.rb +50 -0
- data/lib/parse/api/sessions.rb +24 -0
- data/lib/parse/api/users.rb +250 -0
- data/lib/parse/atlas_search/index_manager.rb +353 -0
- data/lib/parse/atlas_search/result.rb +204 -0
- data/lib/parse/atlas_search/search_builder.rb +604 -0
- data/lib/parse/atlas_search/session.rb +253 -0
- data/lib/parse/atlas_search.rb +995 -0
- data/lib/parse/client/authentication.rb +97 -0
- data/lib/parse/client/batch.rb +234 -0
- data/lib/parse/client/body_builder.rb +240 -0
- data/lib/parse/client/caching.rb +203 -0
- data/lib/parse/client/logging.rb +293 -0
- data/lib/parse/client/profiling.rb +181 -0
- data/lib/parse/client/protocol.rb +91 -0
- data/lib/parse/client/request.rb +233 -0
- data/lib/parse/client/response.rb +208 -0
- data/lib/parse/client.rb +1104 -0
- data/lib/parse/clp_scope.rb +361 -0
- data/lib/parse/live_query/circuit_breaker.rb +256 -0
- data/lib/parse/live_query/client.rb +1001 -0
- data/lib/parse/live_query/configuration.rb +224 -0
- data/lib/parse/live_query/event.rb +115 -0
- data/lib/parse/live_query/event_queue.rb +272 -0
- data/lib/parse/live_query/health_monitor.rb +214 -0
- data/lib/parse/live_query/logging.rb +149 -0
- data/lib/parse/live_query/subscription.rb +294 -0
- data/lib/parse/live_query.rb +163 -0
- data/lib/parse/lookup_rewriter.rb +445 -0
- data/lib/parse/model/acl.rb +968 -0
- data/lib/parse/model/associations/belongs_to.rb +275 -0
- data/lib/parse/model/associations/collection_proxy.rb +435 -0
- data/lib/parse/model/associations/has_many.rb +597 -0
- data/lib/parse/model/associations/has_one.rb +158 -0
- data/lib/parse/model/associations/pointer_collection_proxy.rb +134 -0
- data/lib/parse/model/associations/relation_collection_proxy.rb +177 -0
- data/lib/parse/model/bytes.rb +62 -0
- data/lib/parse/model/classes/audience.rb +262 -0
- data/lib/parse/model/classes/installation.rb +363 -0
- data/lib/parse/model/classes/job_schedule.rb +153 -0
- data/lib/parse/model/classes/job_status.rb +264 -0
- data/lib/parse/model/classes/product.rb +75 -0
- data/lib/parse/model/classes/push_status.rb +263 -0
- data/lib/parse/model/classes/role.rb +751 -0
- data/lib/parse/model/classes/session.rb +201 -0
- data/lib/parse/model/classes/user.rb +943 -0
- data/lib/parse/model/clp.rb +544 -0
- data/lib/parse/model/core/actions.rb +1268 -0
- data/lib/parse/model/core/builder.rb +139 -0
- data/lib/parse/model/core/create_lock.rb +386 -0
- data/lib/parse/model/core/describe.rb +382 -0
- data/lib/parse/model/core/enhanced_change_tracking.rb +159 -0
- data/lib/parse/model/core/errors.rb +38 -0
- data/lib/parse/model/core/fetching.rb +566 -0
- data/lib/parse/model/core/field_guards.rb +220 -0
- data/lib/parse/model/core/indexing.rb +382 -0
- data/lib/parse/model/core/parse_reference.rb +407 -0
- data/lib/parse/model/core/properties.rb +809 -0
- data/lib/parse/model/core/querying.rb +491 -0
- data/lib/parse/model/core/schema.rb +202 -0
- data/lib/parse/model/core/search_indexing.rb +174 -0
- data/lib/parse/model/date.rb +88 -0
- data/lib/parse/model/email.rb +213 -0
- data/lib/parse/model/file.rb +527 -0
- data/lib/parse/model/geojson.rb +271 -0
- data/lib/parse/model/geopoint.rb +261 -0
- data/lib/parse/model/model.rb +260 -0
- data/lib/parse/model/object.rb +2068 -0
- data/lib/parse/model/phone.rb +520 -0
- data/lib/parse/model/pointer.rb +443 -0
- data/lib/parse/model/polygon.rb +406 -0
- data/lib/parse/model/push.rb +975 -0
- data/lib/parse/model/shortnames.rb +8 -0
- data/lib/parse/model/time_zone.rb +141 -0
- data/lib/parse/model/validations/uniqueness_validator.rb +97 -0
- data/lib/parse/model/validations.rb +96 -0
- data/lib/parse/mongodb.rb +2300 -0
- data/lib/parse/pipeline_security.rb +554 -0
- data/lib/parse/query/constraint.rb +198 -0
- data/lib/parse/query/constraints.rb +3279 -0
- data/lib/parse/query/cursor.rb +434 -0
- data/lib/parse/query/n_plus_one_detector.rb +445 -0
- data/lib/parse/query/operation.rb +104 -0
- data/lib/parse/query/ordering.rb +66 -0
- data/lib/parse/query.rb +7028 -0
- data/lib/parse/schema/index_migrator.rb +291 -0
- data/lib/parse/schema/search_index_migrator.rb +289 -0
- data/lib/parse/schema.rb +494 -0
- data/lib/parse/stack/generators/rails.rb +40 -0
- data/lib/parse/stack/generators/templates/model.erb +51 -0
- data/lib/parse/stack/generators/templates/model_installation.rb +4 -0
- data/lib/parse/stack/generators/templates/model_role.rb +4 -0
- data/lib/parse/stack/generators/templates/model_session.rb +4 -0
- data/lib/parse/stack/generators/templates/model_user.rb +11 -0
- data/lib/parse/stack/generators/templates/parse.rb +12 -0
- data/lib/parse/stack/generators/templates/webhooks.rb +10 -0
- data/lib/parse/stack/railtie.rb +18 -0
- data/lib/parse/stack/tasks.rb +563 -0
- data/lib/parse/stack/version.rb +11 -0
- data/lib/parse/stack.rb +455 -0
- data/lib/parse/two_factor_auth/user_extension.rb +449 -0
- data/lib/parse/two_factor_auth.rb +310 -0
- data/lib/parse/webhooks/payload.rb +360 -0
- data/lib/parse/webhooks/registration.rb +199 -0
- data/lib/parse/webhooks/replay_protection.rb +189 -0
- data/lib/parse/webhooks.rb +510 -0
- data/lib/parse-stack-next.rb +5 -0
- data/lib/parse-stack.rb +5 -0
- data/parse-stack-next.gemspec +82 -0
- data/parse-stack.png +0 -0
- data/scripts/debug-ips.js +35 -0
- data/scripts/docker/Dockerfile.parse +13 -0
- data/scripts/docker/atlas-init.js +284 -0
- data/scripts/docker/docker-compose.atlas.yml +76 -0
- data/scripts/docker/docker-compose.test.yml +106 -0
- data/scripts/docker/mongo-init.js +21 -0
- data/scripts/eval_mcp_with_lm_studio.rb +274 -0
- data/scripts/start-parse.sh +90 -0
- data/scripts/start_mcp_server.rb +78 -0
- data/scripts/test_server_connection.rb +82 -0
- metadata +377 -0
|
@@ -0,0 +1,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
|