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,253 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "set"
|
|
5
|
+
require_relative "../clp_scope"
|
|
6
|
+
|
|
7
|
+
module Parse
|
|
8
|
+
module AtlasSearch
|
|
9
|
+
# Resolves session tokens to user identities and inherited role
|
|
10
|
+
# sets for ACL-scoped Atlas Search queries.
|
|
11
|
+
#
|
|
12
|
+
# Atlas Search runs aggregations directly against MongoDB and
|
|
13
|
+
# therefore bypasses Parse Server's per-request ACL enforcement.
|
|
14
|
+
# To compile a +_rperm+ +$match+ stage (see {Parse::ACL.read_predicate})
|
|
15
|
+
# the caller needs to know two things about the requesting
|
|
16
|
+
# session:
|
|
17
|
+
#
|
|
18
|
+
# 1. The +_User.objectId+ that owns the session.
|
|
19
|
+
# 2. The transitive upward closure of role names that user
|
|
20
|
+
# inherits permissions from (cf. {Parse::Role.all_for_user}).
|
|
21
|
+
#
|
|
22
|
+
# Both lookups can be expensive — token → user requires a
|
|
23
|
+
# +/users/me+ round-trip, and user → roles can require multiple
|
|
24
|
+
# +_Role+ queries to walk the inheritance graph. Both are cached
|
|
25
|
+
# separately so a single agent turn that runs several Atlas Search
|
|
26
|
+
# tools amortizes the cost.
|
|
27
|
+
#
|
|
28
|
+
# Two distinct caches:
|
|
29
|
+
#
|
|
30
|
+
# * +session_cache+: maps +session_token+ to +user_id+. Long
|
|
31
|
+
# TTL (1 hour default), invalidation profile is logout. Apps
|
|
32
|
+
# that need sub-TTL revocation must call {.invalidate}
|
|
33
|
+
# explicitly from their logout path.
|
|
34
|
+
#
|
|
35
|
+
# * +role_cache+: maps +user_id+ to a +Set+ of role names. Short
|
|
36
|
+
# TTL (2 minutes default), invalidation profile is role-graph
|
|
37
|
+
# mutation. Stale role data here yields incorrect ACL
|
|
38
|
+
# decisions, so the default is conservatively short.
|
|
39
|
+
#
|
|
40
|
+
# The default cache implementation is process-local
|
|
41
|
+
# ({MemoryCache}) and guarded by a +Mutex+. Apps that need shared
|
|
42
|
+
# cross-process caching (Redis, Memcached) may install a
|
|
43
|
+
# replacement via {AtlasSearch.session_cache=} /
|
|
44
|
+
# {AtlasSearch.role_cache=}; the replacement must respond to
|
|
45
|
+
# +get(key)+, +set(key, value, ttl:)+, and +invalidate(key)+.
|
|
46
|
+
module Session
|
|
47
|
+
# Raised when a +session_token+ cannot be resolved — invalid
|
|
48
|
+
# token, expired session, or +/users/me+ returned an error.
|
|
49
|
+
# Atlas Search callers should treat this as a 401-equivalent.
|
|
50
|
+
class InvalidSession < StandardError; end
|
|
51
|
+
|
|
52
|
+
# Default cache: in-memory hash with per-entry TTL, guarded by a
|
|
53
|
+
# +Mutex+. Suitable for single-process apps. Apps running
|
|
54
|
+
# multi-process (Puma workers, Sidekiq processes) get a per-
|
|
55
|
+
# process cache — install a shared cache through
|
|
56
|
+
# {AtlasSearch.session_cache=} for cross-process sharing.
|
|
57
|
+
class MemoryCache
|
|
58
|
+
def initialize
|
|
59
|
+
@data = {}
|
|
60
|
+
@mutex = Mutex.new
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# @param key [String]
|
|
64
|
+
# @return [Object, nil] the cached value, or +nil+ when the key
|
|
65
|
+
# is missing or its TTL has elapsed. Expired entries are
|
|
66
|
+
# evicted lazily on read.
|
|
67
|
+
def get(key)
|
|
68
|
+
@mutex.synchronize do
|
|
69
|
+
entry = @data[key]
|
|
70
|
+
return nil if entry.nil?
|
|
71
|
+
if entry[:expires_at] < Time.now
|
|
72
|
+
@data.delete(key)
|
|
73
|
+
return nil
|
|
74
|
+
end
|
|
75
|
+
entry[:value]
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# @param key [String]
|
|
80
|
+
# @param value [Object]
|
|
81
|
+
# @param ttl [Numeric] seconds until the entry expires.
|
|
82
|
+
def set(key, value, ttl:)
|
|
83
|
+
@mutex.synchronize do
|
|
84
|
+
@data[key] = { value: value, expires_at: Time.now + ttl }
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# @param key [String] cache key to forget.
|
|
89
|
+
def invalidate(key)
|
|
90
|
+
@mutex.synchronize { @data.delete(key) }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Drop every entry. Used by {Session.reset_caches!} and by
|
|
94
|
+
# tests that need a clean slate.
|
|
95
|
+
def clear
|
|
96
|
+
@mutex.synchronize { @data.clear }
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Value returned by {Session.resolve}. +user_id+ is the
|
|
101
|
+
# +_User.objectId+ owning the session, or +nil+ for an anonymous
|
|
102
|
+
# caller. +role_names+ is a +Set+ of role names (no +role:+
|
|
103
|
+
# prefix) the user inherits permissions from, computed via
|
|
104
|
+
# {Parse::Role.all_for_user}.
|
|
105
|
+
Resolved = Struct.new(:user_id, :role_names) do
|
|
106
|
+
# Build the canonical +_rperm+/+_wperm+ permission-string set
|
|
107
|
+
# for this session. Always includes +"*"+ (public). Includes
|
|
108
|
+
# +user_id+ when present. Includes +"role:#{name}"+ for each
|
|
109
|
+
# inherited role.
|
|
110
|
+
# @return [Array<String>]
|
|
111
|
+
def permission_strings
|
|
112
|
+
out = ["*"]
|
|
113
|
+
out << user_id if user_id && !user_id.empty?
|
|
114
|
+
role_names.each { |name| out << "role:#{name}" if name && !name.empty? }
|
|
115
|
+
out.uniq
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# @return [Boolean] +true+ for the anonymous-session case.
|
|
119
|
+
def anonymous?
|
|
120
|
+
user_id.nil? || user_id.empty?
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
class << self
|
|
125
|
+
# Resolve a +session_token+ to the requesting user and the
|
|
126
|
+
# transitive set of role names whose +role:NAME+ permission
|
|
127
|
+
# strings should be checked against +_rperm+.
|
|
128
|
+
#
|
|
129
|
+
# +nil+ or empty +session_token+ → anonymous {Resolved} with
|
|
130
|
+
# +user_id: nil+ and an empty +role_names+ set. The caller
|
|
131
|
+
# decides whether to refuse the request (the
|
|
132
|
+
# +require_session_token+ toggle on {Parse::AtlasSearch}) or
|
|
133
|
+
# treat as public-only.
|
|
134
|
+
#
|
|
135
|
+
# Cache layering: token-to-user_id is checked first; on hit
|
|
136
|
+
# the slower +/users/me+ round-trip is skipped. User-to-roles
|
|
137
|
+
# is then checked independently (a single user shared across
|
|
138
|
+
# sessions amortizes the role lookup).
|
|
139
|
+
#
|
|
140
|
+
# @param session_token [String, nil] the +X-Parse-Session-Token+
|
|
141
|
+
# value from the requesting session.
|
|
142
|
+
# @return [Resolved]
|
|
143
|
+
# @raise [InvalidSession] when the token cannot be resolved by
|
|
144
|
+
# +/users/me+ (404 / 209 invalid session token / 401).
|
|
145
|
+
def resolve(session_token)
|
|
146
|
+
return Resolved.new(nil, Set.new) if session_token.nil? || session_token.to_s.empty?
|
|
147
|
+
|
|
148
|
+
user_id = lookup_user_id(session_token.to_s)
|
|
149
|
+
role_names = lookup_role_names(user_id)
|
|
150
|
+
Resolved.new(user_id, role_names)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Forget a +session_token+ entry from the session-token cache.
|
|
154
|
+
# Apps that revoke sessions out-of-band (logout, password
|
|
155
|
+
# reset, admin revoke) should call this from the same path so
|
|
156
|
+
# subsequent Atlas Search requests don't act on the stale
|
|
157
|
+
# +user_id+ mapping. The +role_names+ cache is keyed on
|
|
158
|
+
# +user_id+ and is not affected — call {.invalidate_user_roles}
|
|
159
|
+
# to clear that separately.
|
|
160
|
+
# @param session_token [String]
|
|
161
|
+
def invalidate(session_token)
|
|
162
|
+
return if session_token.nil?
|
|
163
|
+
Parse::AtlasSearch.session_cache.invalidate(session_token.to_s)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Forget cached role membership for a +user_id+. Call after any
|
|
167
|
+
# +_Role.users+ mutation that affects this user (role grant /
|
|
168
|
+
# revoke, role-graph reshape).
|
|
169
|
+
# @param user_id [String]
|
|
170
|
+
def invalidate_user_roles(user_id)
|
|
171
|
+
return if user_id.nil?
|
|
172
|
+
Parse::AtlasSearch.role_cache.invalidate(user_id.to_s)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Drop every cached entry across both caches. Useful in tests
|
|
176
|
+
# and in startup hooks for processes that fork after warming
|
|
177
|
+
# the cache.
|
|
178
|
+
def reset_caches!
|
|
179
|
+
Parse::AtlasSearch.session_cache.clear if Parse::AtlasSearch.session_cache.respond_to?(:clear)
|
|
180
|
+
Parse::AtlasSearch.role_cache.clear if Parse::AtlasSearch.role_cache.respond_to?(:clear)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
private
|
|
184
|
+
|
|
185
|
+
# @!visibility private
|
|
186
|
+
# Resolve session_token → user_id via cache, falling through
|
|
187
|
+
# to +/users/me+. Raises {InvalidSession} on lookup failure;
|
|
188
|
+
# the caller is responsible for refusing the request.
|
|
189
|
+
def lookup_user_id(session_token)
|
|
190
|
+
cache = Parse::AtlasSearch.session_cache
|
|
191
|
+
cached = cache.get(session_token)
|
|
192
|
+
return cached if cached
|
|
193
|
+
|
|
194
|
+
response = begin
|
|
195
|
+
Parse.client.current_user(session_token)
|
|
196
|
+
rescue => e
|
|
197
|
+
raise InvalidSession, "session token lookup failed: #{e.class}: #{e.message}"
|
|
198
|
+
end
|
|
199
|
+
raise InvalidSession, "session token invalid or expired" if response.nil? || response.error?
|
|
200
|
+
|
|
201
|
+
result = response.result
|
|
202
|
+
user_id = result.is_a?(Hash) ? (result["objectId"] || result[:objectId]) : nil
|
|
203
|
+
raise InvalidSession, "session token resolved no user objectId" if user_id.nil? || user_id.to_s.empty?
|
|
204
|
+
|
|
205
|
+
user_id = user_id.to_s
|
|
206
|
+
cache.set(session_token, user_id, ttl: Parse::AtlasSearch.session_cache_ttl)
|
|
207
|
+
user_id
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# @!visibility private
|
|
211
|
+
# Resolve user_id → Set<role_name> via cache, falling through
|
|
212
|
+
# to {Parse::Role.all_for_user}. Failures degrade silently to
|
|
213
|
+
# an empty set rather than raising — a Parse Server hiccup
|
|
214
|
+
# during the role walk must not turn every search call into a
|
|
215
|
+
# 500, and the worst case is a query that misses some
|
|
216
|
+
# role-restricted documents.
|
|
217
|
+
#
|
|
218
|
+
# ATLAS-7: explicitly re-raise the exceptions that signal
|
|
219
|
+
# attacks or policy denials BEFORE the generic rescue. Without
|
|
220
|
+
# this, a denied-operator probe (DeniedOperator), a timeout
|
|
221
|
+
# exhaustion (ExecutionTimeout), or a CLP denial during role
|
|
222
|
+
# graph traversal would silently downgrade to an empty role
|
|
223
|
+
# set — the caller would then run with public-only perms,
|
|
224
|
+
# missing role-restricted rows but also masking the attack
|
|
225
|
+
# signal from the operator. These exception classes are SDK
|
|
226
|
+
# contracts the caller must surface upward.
|
|
227
|
+
def lookup_role_names(user_id)
|
|
228
|
+
return Set.new if user_id.nil? || user_id.empty?
|
|
229
|
+
|
|
230
|
+
cache = Parse::AtlasSearch.role_cache
|
|
231
|
+
cached = cache.get(user_id)
|
|
232
|
+
return cached if cached.is_a?(Set)
|
|
233
|
+
|
|
234
|
+
pointer = Parse::Pointer.new(Parse::Model::CLASS_USER, user_id)
|
|
235
|
+
names = begin
|
|
236
|
+
Parse::Role.all_for_user(pointer, max_depth: 10)
|
|
237
|
+
rescue Parse::MongoDB::DeniedOperator,
|
|
238
|
+
Parse::MongoDB::ExecutionTimeout,
|
|
239
|
+
Parse::CLPScope::Denied
|
|
240
|
+
# Re-raise: these are attack signals or explicit policy
|
|
241
|
+
# denials and must NOT be swallowed into a fail-open
|
|
242
|
+
# public-only ACL state.
|
|
243
|
+
raise
|
|
244
|
+
rescue
|
|
245
|
+
Set.new
|
|
246
|
+
end
|
|
247
|
+
cache.set(user_id, names, ttl: Parse::AtlasSearch.role_cache_ttl)
|
|
248
|
+
names
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|