parse-stack-next 5.5.6 → 5.7.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 +4 -4
- data/CHANGELOG.md +634 -0
- data/README.md +378 -6
- data/docs/caching.md +748 -0
- data/examples/basic_client.rb +3 -3
- data/examples/basic_server.rb +3 -3
- data/examples/live_query_listener.rb +12 -12
- data/examples/rag_chatbot.rb +14 -14
- data/examples/transaction_example.rb +44 -45
- data/examples/webhook_server.rb +3 -3
- data/lib/parse/access.rb +493 -0
- data/lib/parse/acl_scope.rb +125 -52
- data/lib/parse/agent/approval_gate.rb +0 -0
- data/lib/parse/agent/cancellation_token.rb +3 -3
- data/lib/parse/agent/constraint_translator.rb +4 -4
- data/lib/parse/agent/describe.rb +34 -34
- data/lib/parse/agent/errors.rb +9 -9
- data/lib/parse/agent/mcp_client.rb +61 -59
- data/lib/parse/agent/mcp_dispatcher.rb +89 -101
- data/lib/parse/agent/mcp_rack_app.rb +92 -93
- data/lib/parse/agent/mcp_server.rb +1 -1
- data/lib/parse/agent/mcp_subscriptions.rb +22 -22
- data/lib/parse/agent/metadata_audit.rb +1 -2
- data/lib/parse/agent/metadata_dsl.rb +17 -7
- data/lib/parse/agent/metadata_registry.rb +12 -12
- data/lib/parse/agent/prompt_hardening.rb +4 -4
- data/lib/parse/agent/prompts.rb +10 -10
- data/lib/parse/agent/result_formatter.rb +4 -4
- data/lib/parse/agent/tools.rb +493 -400
- data/lib/parse/agent.rb +157 -116
- data/lib/parse/api/hooks.rb +1 -1
- data/lib/parse/api/server.rb +2 -2
- data/lib/parse/api/users.rb +2 -2
- data/lib/parse/atlas_search/index_manager.rb +1 -1
- data/lib/parse/atlas_search/session.rb +40 -218
- data/lib/parse/atlas_search.rb +181 -60
- data/lib/parse/authorization.rb +466 -0
- data/lib/parse/cache/invalidation.rb +167 -0
- data/lib/parse/cache/keyspace.rb +306 -0
- data/lib/parse/cache/moneta_surface.rb +126 -0
- data/lib/parse/cache/pool.rb +48 -5
- data/lib/parse/cache/redis.rb +425 -10
- data/lib/parse/cache/scoped_view.rb +443 -0
- data/lib/parse/cache/sub_cache.rb +264 -0
- data/lib/parse/cache/upstream_roles.rb +230 -0
- data/lib/parse/client/authentication.rb +1 -1
- data/lib/parse/client/body_builder.rb +1 -2
- data/lib/parse/client/caching.rb +84 -5
- data/lib/parse/client.rb +327 -189
- data/lib/parse/clp_scope.rb +225 -28
- data/lib/parse/console.rb +3 -3
- data/lib/parse/embeddings/batch_embedder.rb +3 -3
- data/lib/parse/embeddings/binding_audit.rb +211 -0
- data/lib/parse/embeddings/cache.rb +17 -17
- data/lib/parse/embeddings/cohere.rb +21 -22
- data/lib/parse/embeddings/image_fetch.rb +22 -21
- data/lib/parse/embeddings/jina.rb +21 -21
- data/lib/parse/embeddings/local_http.rb +6 -7
- data/lib/parse/embeddings/media_file.rb +137 -0
- data/lib/parse/embeddings/openai.rb +5 -5
- data/lib/parse/embeddings/provider.rb +40 -1
- data/lib/parse/embeddings/qwen.rb +10 -10
- data/lib/parse/embeddings/spend_cap.rb +2 -3
- data/lib/parse/embeddings/streaming_body.rb +170 -0
- data/lib/parse/embeddings/video_source.rb +121 -0
- data/lib/parse/embeddings/voyage.rb +554 -143
- data/lib/parse/embeddings.rb +83 -27
- data/lib/parse/graphql/type_generator.rb +1 -1
- data/lib/parse/graphql.rb +5 -5
- data/lib/parse/live_query/client.rb +1 -1
- data/lib/parse/live_query.rb +1 -1
- data/lib/parse/lock.rb +15 -18
- data/lib/parse/lock_backend.rb +1 -1
- data/lib/parse/lookup_rewriter.rb +6 -6
- data/lib/parse/model/classes/role.rb +227 -28
- data/lib/parse/model/classes/user.rb +60 -1
- data/lib/parse/model/clp.rb +4 -4
- data/lib/parse/model/core/actions.rb +97 -15
- data/lib/parse/model/core/create_lock.rb +0 -2
- data/lib/parse/model/core/describe.rb +57 -57
- data/lib/parse/model/core/embed_managed.rb +43 -19
- data/lib/parse/model/core/fetching.rb +2 -0
- data/lib/parse/model/core/indexing.rb +14 -14
- data/lib/parse/model/core/parse_reference.rb +1 -1
- data/lib/parse/model/core/properties.rb +39 -3
- data/lib/parse/model/core/querying.rb +1 -1
- data/lib/parse/model/core/schema.rb +2 -2
- data/lib/parse/model/core/search_indexing.rb +2 -2
- data/lib/parse/model/core/vector_searchable.rb +34 -8
- data/lib/parse/model/file.rb +21 -15
- data/lib/parse/model/geojson.rb +2 -2
- data/lib/parse/model/geopoint.rb +1 -0
- data/lib/parse/model/object.rb +132 -66
- data/lib/parse/model/pointer.rb +2 -0
- data/lib/parse/model/polygon.rb +3 -6
- data/lib/parse/model/push.rb +2 -2
- data/lib/parse/model/vector.rb +3 -1
- data/lib/parse/mongodb.rb +512 -282
- data/lib/parse/pipeline_security.rb +3 -0
- data/lib/parse/query/constraints.rb +46 -46
- data/lib/parse/query.rb +146 -59
- data/lib/parse/retrieval/agent_tool.rb +34 -20
- data/lib/parse/retrieval/chunk.rb +1 -0
- data/lib/parse/retrieval/reranker/cohere.rb +11 -11
- data/lib/parse/retrieval/reranker.rb +3 -4
- data/lib/parse/retrieval/retriever.rb +4 -6
- data/lib/parse/schema/index_migrator.rb +26 -26
- data/lib/parse/schema/search_index_migrator.rb +19 -19
- data/lib/parse/stack/tasks.rb +6 -6
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +10 -10
- data/lib/parse/vector_search/hybrid.rb +166 -33
- data/lib/parse/vector_search.rb +141 -13
- data/lib/parse/webhooks/payload.rb +1 -0
- data/lib/parse/webhooks/registration.rb +4 -4
- data/lib/parse/webhooks/trigger_audit.rb +50 -49
- data/lib/parse/webhooks.rb +22 -4
- data/parse-stack-next.gemspec +6 -6
- metadata +14 -1
|
@@ -65,6 +65,7 @@ module Parse
|
|
|
65
65
|
class IndexDriftError < StandardError
|
|
66
66
|
# @return [Array<String>] human-readable drift findings.
|
|
67
67
|
attr_reader :findings
|
|
68
|
+
|
|
68
69
|
def initialize(message, findings: [])
|
|
69
70
|
@findings = findings
|
|
70
71
|
super(message)
|
|
@@ -149,6 +150,12 @@ module Parse
|
|
|
149
150
|
# (fields must be declared `type: "filter"` in the index).
|
|
150
151
|
# @param index [String, nil] explicit vectorSearch index name.
|
|
151
152
|
# Skips auto-discovery when given.
|
|
153
|
+
# @param candidate_limit [Integer, nil] rows Atlas returns before
|
|
154
|
+
# ACL / filter / pointer-field enforcement runs; the result is
|
|
155
|
+
# trimmed to `k` afterwards. Defaults to a multiple of `k` for
|
|
156
|
+
# any caller subject to enforcement, so heavy ACL attrition
|
|
157
|
+
# does not silently return fewer than `k` rows. Raise it when
|
|
158
|
+
# a principal can read only a small fraction of the class.
|
|
152
159
|
# @param num_candidates [Integer, nil] HNSW search width.
|
|
153
160
|
# @param max_time_ms [Integer, nil] server-side timeout.
|
|
154
161
|
# @param raw [Boolean] when true return the raw Mongo documents
|
|
@@ -192,7 +199,8 @@ module Parse
|
|
|
192
199
|
# deadline on the embed step.
|
|
193
200
|
def find_similar(vector: nil, text: nil, k: 10, field: nil, filter: nil,
|
|
194
201
|
vector_filter: nil, index: nil,
|
|
195
|
-
num_candidates: nil,
|
|
202
|
+
num_candidates: nil, candidate_limit: nil,
|
|
203
|
+
max_time_ms: nil, raw: false,
|
|
196
204
|
**scope_opts)
|
|
197
205
|
if vector.nil? && text.nil?
|
|
198
206
|
raise ArgumentError,
|
|
@@ -206,8 +214,7 @@ module Parse
|
|
|
206
214
|
resolved_field = resolve_vector_field!(field)
|
|
207
215
|
declared_dims = vector_properties.dig(resolved_field, :dimensions)
|
|
208
216
|
|
|
209
|
-
query_vector =
|
|
210
|
-
if text.nil?
|
|
217
|
+
query_vector = if text.nil?
|
|
211
218
|
coerce_query_vector(vector)
|
|
212
219
|
else
|
|
213
220
|
embed_query_text!(text, resolved_field)
|
|
@@ -222,6 +229,7 @@ module Parse
|
|
|
222
229
|
query_vector: query_vector,
|
|
223
230
|
k: k,
|
|
224
231
|
num_candidates: num_candidates,
|
|
232
|
+
candidate_limit: candidate_limit,
|
|
225
233
|
filter: filter,
|
|
226
234
|
vector_filter: vector_filter,
|
|
227
235
|
index: index_name,
|
|
@@ -284,8 +292,7 @@ module Parse
|
|
|
284
292
|
declared_dims = vector_properties.dig(field_sym, :dimensions)
|
|
285
293
|
|
|
286
294
|
qv = query_vector || vec[:query_vector]
|
|
287
|
-
qv =
|
|
288
|
-
if qv.nil?
|
|
295
|
+
qv = if qv.nil?
|
|
289
296
|
unless text.is_a?(String) && !text.strip.empty?
|
|
290
297
|
raise ArgumentError,
|
|
291
298
|
"#{self}.hybrid_search: pass `text:` (to embed) or a `query_vector:`."
|
|
@@ -314,6 +321,7 @@ module Parse
|
|
|
314
321
|
query_vector: qv, field: field_sym, index: vector_index,
|
|
315
322
|
num_candidates: vec[:num_candidates], filter: vec[:filter],
|
|
316
323
|
vector_filter: vec[:vector_filter],
|
|
324
|
+
candidate_limit: vec[:candidate_limit],
|
|
317
325
|
},
|
|
318
326
|
k: k,
|
|
319
327
|
fusion: fusion,
|
|
@@ -327,9 +335,22 @@ module Parse
|
|
|
327
335
|
private
|
|
328
336
|
|
|
329
337
|
def resolve_vector_field!(field)
|
|
330
|
-
declared
|
|
338
|
+
# A property declared `searchable: false` is storage-only. That
|
|
339
|
+
# may be because it exceeds the Atlas index cap, or simply
|
|
340
|
+
# because the author opted it out. Either way, including it in
|
|
341
|
+
# resolution would let a caller reach a field no index serves,
|
|
342
|
+
# so it is excluded from auto-resolution entirely and refused
|
|
343
|
+
# with an explanation when named outright.
|
|
344
|
+
all_declared = vector_properties.keys
|
|
345
|
+
declared = all_declared.reject { |f| vector_properties[f][:searchable] == false }
|
|
346
|
+
|
|
331
347
|
if field
|
|
332
348
|
sym = field.to_sym
|
|
349
|
+
if all_declared.include?(sym) && !declared.include?(sym)
|
|
350
|
+
raise NoVectorProperty,
|
|
351
|
+
"#{self}.find_similar: field :#{sym} is declared `searchable: false` " \
|
|
352
|
+
"(storage-only, not eligible for search) and cannot be searched."
|
|
353
|
+
end
|
|
333
354
|
unless declared.include?(sym)
|
|
334
355
|
raise NoVectorProperty,
|
|
335
356
|
"#{self}.find_similar: field :#{sym} is not a :vector property " \
|
|
@@ -342,7 +363,7 @@ module Parse
|
|
|
342
363
|
end
|
|
343
364
|
if declared.empty?
|
|
344
365
|
raise NoVectorProperty,
|
|
345
|
-
"#{self}.find_similar: no :vector property declared on this class."
|
|
366
|
+
"#{self}.find_similar: no searchable :vector property declared on this class."
|
|
346
367
|
end
|
|
347
368
|
raise AmbiguousVectorField,
|
|
348
369
|
"#{self}.find_similar: class declares multiple :vector properties " \
|
|
@@ -383,6 +404,11 @@ module Parse
|
|
|
383
404
|
"on the property, or pass an explicit `vector:`."
|
|
384
405
|
end
|
|
385
406
|
provider = Parse::Embeddings.provider(provider_name)
|
|
407
|
+
# A query embedded by a different model than the stored vectors
|
|
408
|
+
# returns plausible-looking nonsense rather than an error, so
|
|
409
|
+
# the binding is verified here too — before the spend cap is
|
|
410
|
+
# charged and before any provider round-trip.
|
|
411
|
+
Parse::Embeddings::BindingAudit.verify!(self, resolved_field, provider)
|
|
386
412
|
# Spend cap: every query-embed path (find_similar(text:),
|
|
387
413
|
# hybrid_search(text:), Retrieval.retrieve) funnels through this
|
|
388
414
|
# method, so charging here closes the "direct callers bypass the
|
|
@@ -405,7 +431,7 @@ module Parse
|
|
|
405
431
|
def coerce_query_vector(vector)
|
|
406
432
|
case vector
|
|
407
433
|
when Parse::Vector then vector.to_a
|
|
408
|
-
when Array
|
|
434
|
+
when Array then vector
|
|
409
435
|
else
|
|
410
436
|
raise Parse::VectorSearch::InvalidQueryVector,
|
|
411
437
|
"vector: must be an Array<Float> or Parse::Vector (got #{vector.class})."
|
data/lib/parse/model/file.rb
CHANGED
|
@@ -43,7 +43,6 @@ module Parse
|
|
|
43
43
|
# in logs or a CDN access trail.
|
|
44
44
|
class SignedUrlError < Parse::Error; end
|
|
45
45
|
|
|
46
|
-
|
|
47
46
|
# Regular expression that matches the old legacy Parse hosted file name
|
|
48
47
|
LEGACY_FILE_RX = /^tfss-[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}-/
|
|
49
48
|
# The default attributes in a Parse File hash. Matches the Parse
|
|
@@ -86,7 +85,7 @@ module Parse
|
|
|
86
85
|
# Alibaba Cloud metadata service (public-IP-space but well-known
|
|
87
86
|
# cloud-metadata endpoint that must not be reachable from SDK fetches).
|
|
88
87
|
"100.100.100.200/32",
|
|
89
|
-
"::/128", "::1/128", "fc00::/7", "fe80::/10", "ff00::/8", "::ffff:0:0/96"
|
|
88
|
+
"::/128", "::1/128", "fc00::/7", "fe80::/10", "ff00::/8", "::ffff:0:0/96",
|
|
90
89
|
].map { |c| IPAddr.new(c) }.freeze
|
|
91
90
|
# Restrictive port allowlist for Parse::File URL fetches. By default
|
|
92
91
|
# only the standard HTTP/HTTPS ports are permitted. Operators may
|
|
@@ -166,12 +165,14 @@ module Parse
|
|
|
166
165
|
# @return [Integer] Maximum byte size for a remote URL fetch via
|
|
167
166
|
# `Parse::File.create` / `Parse::File.new(url)`.
|
|
168
167
|
attr_writer :max_remote_size
|
|
168
|
+
|
|
169
169
|
def max_remote_size
|
|
170
170
|
@max_remote_size ||= DEFAULT_MAX_REMOTE_SIZE
|
|
171
171
|
end
|
|
172
172
|
|
|
173
173
|
# @return [Integer] Read/open timeout (seconds) for remote URL fetches.
|
|
174
174
|
attr_writer :remote_timeout
|
|
175
|
+
|
|
175
176
|
def remote_timeout
|
|
176
177
|
@remote_timeout ||= DEFAULT_REMOTE_TIMEOUT
|
|
177
178
|
end
|
|
@@ -182,6 +183,7 @@ module Parse
|
|
|
182
183
|
# ".example.com" matches "files.example.com"). Default: empty (any
|
|
183
184
|
# public host is allowed; private hosts are always denied).
|
|
184
185
|
attr_writer :allowed_remote_hosts
|
|
186
|
+
|
|
185
187
|
def allowed_remote_hosts
|
|
186
188
|
@allowed_remote_hosts ||= []
|
|
187
189
|
end
|
|
@@ -202,6 +204,7 @@ module Parse
|
|
|
202
204
|
# entries via leading "." (e.g. `".cdn.example.com"`) match any
|
|
203
205
|
# subdomain.
|
|
204
206
|
attr_writer :trusted_url_hosts
|
|
207
|
+
|
|
205
208
|
def trusted_url_hosts
|
|
206
209
|
@trusted_url_hosts ||= ["files.parsetfss.com"]
|
|
207
210
|
end
|
|
@@ -226,6 +229,7 @@ module Parse
|
|
|
226
229
|
# hydration `attributes=` — asymmetric writer behavior was an
|
|
227
230
|
# explicit anti-goal of the design.
|
|
228
231
|
attr_writer :signed_url_policy
|
|
232
|
+
|
|
229
233
|
def signed_url_policy
|
|
230
234
|
@signed_url_policy ||= :strip
|
|
231
235
|
end
|
|
@@ -244,12 +248,14 @@ module Parse
|
|
|
244
248
|
# The default is intentionally non-breaking; integrators ready to
|
|
245
249
|
# enforce flip the policy explicitly.
|
|
246
250
|
attr_writer :untrusted_url_policy
|
|
251
|
+
|
|
247
252
|
def untrusted_url_policy
|
|
248
253
|
@untrusted_url_policy ||= :warn
|
|
249
254
|
end
|
|
250
255
|
|
|
251
256
|
# @return [Array<Integer>] Allowed remote ports for URL fetches.
|
|
252
257
|
attr_writer :allowed_remote_ports
|
|
258
|
+
|
|
253
259
|
def allowed_remote_ports
|
|
254
260
|
@allowed_remote_ports ||= DEFAULT_ALLOWED_REMOTE_PORTS.dup
|
|
255
261
|
end
|
|
@@ -543,10 +549,10 @@ module Parse
|
|
|
543
549
|
# "exceeds". Fail fast with a clear message before any DNS/host work.
|
|
544
550
|
max_bytes = coerce_positive_max_bytes(max_bytes)
|
|
545
551
|
uri = begin
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
552
|
+
URI.parse(url_string)
|
|
553
|
+
rescue URI::InvalidURIError => e
|
|
554
|
+
raise ArgumentError, "Invalid URL: #{e.message}"
|
|
555
|
+
end
|
|
550
556
|
unless %w[http https].include?(uri.scheme)
|
|
551
557
|
raise ArgumentError, "Parse::File only supports http(s) URLs (got #{uri.scheme.inspect})"
|
|
552
558
|
end
|
|
@@ -603,8 +609,7 @@ module Parse
|
|
|
603
609
|
# @return [Integer, nil]
|
|
604
610
|
def coerce_positive_max_bytes(max_bytes)
|
|
605
611
|
return nil if max_bytes.nil?
|
|
606
|
-
requested =
|
|
607
|
-
begin
|
|
612
|
+
requested = begin
|
|
608
613
|
Integer(max_bytes)
|
|
609
614
|
rescue ArgumentError, TypeError
|
|
610
615
|
raise ArgumentError, "max_bytes must be a positive integer (got #{max_bytes.inspect})"
|
|
@@ -931,6 +936,7 @@ module Parse
|
|
|
931
936
|
@url = Parse::File.sanitize_hydrated_url(value, fallback: @url, name: @name)
|
|
932
937
|
end
|
|
933
938
|
end
|
|
939
|
+
|
|
934
940
|
private :normalize_and_store_url
|
|
935
941
|
|
|
936
942
|
# @!visibility private
|
|
@@ -953,10 +959,10 @@ module Parse
|
|
|
953
959
|
return raw unless raw.start_with?("http://") || raw.start_with?("https://")
|
|
954
960
|
|
|
955
961
|
uri = begin
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
962
|
+
URI.parse(raw)
|
|
963
|
+
rescue URI::InvalidURIError
|
|
964
|
+
return raw # malformed URL — leave it alone; downstream code already handles
|
|
965
|
+
end
|
|
960
966
|
host = uri.host.to_s.downcase
|
|
961
967
|
return raw if host.empty?
|
|
962
968
|
|
|
@@ -975,7 +981,7 @@ module Parse
|
|
|
975
981
|
when :strip
|
|
976
982
|
warn_untrusted_url_host_once(host, action: "stripped")
|
|
977
983
|
fallback
|
|
978
|
-
else
|
|
984
|
+
else # :warn (default)
|
|
979
985
|
warn_untrusted_url_host_once(host, action: "accepted")
|
|
980
986
|
raw
|
|
981
987
|
end
|
|
@@ -1034,8 +1040,8 @@ module Parse
|
|
|
1034
1040
|
def save(session_token: nil, use_master_key: nil)
|
|
1035
1041
|
unless saved? || @contents.nil? || @name.nil?
|
|
1036
1042
|
opts = {}
|
|
1037
|
-
opts[:session_token]
|
|
1038
|
-
opts[:use_master_key]
|
|
1043
|
+
opts[:session_token] = session_token unless session_token.nil?
|
|
1044
|
+
opts[:use_master_key] = use_master_key unless use_master_key.nil?
|
|
1039
1045
|
response = client.create_file(@name, @contents, @mime_type, **opts)
|
|
1040
1046
|
unless response.error?
|
|
1041
1047
|
result = response.result
|
data/lib/parse/model/geojson.rb
CHANGED
|
@@ -49,8 +49,7 @@ module Parse
|
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
def coordinates=(value)
|
|
52
|
-
coords =
|
|
53
|
-
case value
|
|
52
|
+
coords = case value
|
|
54
53
|
when self.class
|
|
55
54
|
deep_copy_array(value.coordinates)
|
|
56
55
|
when Hash
|
|
@@ -76,6 +75,7 @@ module Parse
|
|
|
76
75
|
def to_geojson
|
|
77
76
|
{ "type" => geojson_type, "coordinates" => deep_copy_array(@coordinates) }
|
|
78
77
|
end
|
|
78
|
+
|
|
79
79
|
alias_method :as_json, :to_geojson
|
|
80
80
|
|
|
81
81
|
# @return [String] the JSON form, suitable for direct shipment to
|
data/lib/parse/model/geopoint.rb
CHANGED
data/lib/parse/model/object.rb
CHANGED
|
@@ -403,11 +403,11 @@ module Parse
|
|
|
403
403
|
# @return [Parse::ACL] the current default ACLs for this class.
|
|
404
404
|
def default_acls
|
|
405
405
|
@default_acls ||= case acl_policy_setting
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
406
|
+
when :public, :owner_else_public then Parse::ACL.everyone
|
|
407
|
+
when :public_read, :owner_but_public_read then Parse::ACL.everyone(true, false)
|
|
408
|
+
when :private, :owner_else_private then Parse::ACL.private
|
|
409
|
+
else Parse::ACL.everyone
|
|
410
|
+
end
|
|
411
411
|
end
|
|
412
412
|
|
|
413
413
|
# A method to set default ACLs to be applied for newly created
|
|
@@ -541,10 +541,10 @@ module Parse
|
|
|
541
541
|
end
|
|
542
542
|
if owner.nil? && policy.to_s.start_with?("owner_")
|
|
543
543
|
fallback = case policy
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
544
|
+
when :owner_else_public then "public R/W"
|
|
545
|
+
when :owner_but_public_read then "public read only"
|
|
546
|
+
else "master-key-only"
|
|
547
|
+
end
|
|
548
548
|
warn "[#{self}] acl_policy #{policy.inspect} declared without `owner:` field; ACL resolution will always use the fallback (#{fallback}). Pass `as:` at construction to override."
|
|
549
549
|
end
|
|
550
550
|
@acl_policy_setting = policy
|
|
@@ -698,7 +698,7 @@ module Parse
|
|
|
698
698
|
class_permissions.set_default_permission(
|
|
699
699
|
public_access: public,
|
|
700
700
|
roles: Array(roles),
|
|
701
|
-
requires_authentication: requires_authentication
|
|
701
|
+
requires_authentication: requires_authentication,
|
|
702
702
|
)
|
|
703
703
|
|
|
704
704
|
# Also explicitly set all operations to ensure they're included
|
|
@@ -789,7 +789,7 @@ module Parse
|
|
|
789
789
|
roles: Array(roles),
|
|
790
790
|
users: Array(users),
|
|
791
791
|
pointer_fields: converted_pointer_fields,
|
|
792
|
-
requires_authentication: requires_authentication
|
|
792
|
+
requires_authentication: requires_authentication,
|
|
793
793
|
)
|
|
794
794
|
end
|
|
795
795
|
|
|
@@ -1043,8 +1043,8 @@ module Parse
|
|
|
1043
1043
|
|
|
1044
1044
|
per_field[local_sym] = {
|
|
1045
1045
|
write: guards_map[local_sym] || :open,
|
|
1046
|
-
read:
|
|
1047
|
-
type:
|
|
1046
|
+
read: hidden_from.empty? ? :open : { hidden_from: hidden_from },
|
|
1047
|
+
type: data_type,
|
|
1048
1048
|
}
|
|
1049
1049
|
end
|
|
1050
1050
|
|
|
@@ -1057,10 +1057,10 @@ module Parse
|
|
|
1057
1057
|
end
|
|
1058
1058
|
|
|
1059
1059
|
{
|
|
1060
|
-
operations:
|
|
1061
|
-
read_user_fields:
|
|
1060
|
+
operations: operations,
|
|
1061
|
+
read_user_fields: perms.respond_to?(:read_user_fields) ? perms.read_user_fields : [],
|
|
1062
1062
|
write_user_fields: perms.respond_to?(:write_user_fields) ? perms.write_user_fields : [],
|
|
1063
|
-
fields:
|
|
1063
|
+
fields: per_field,
|
|
1064
1064
|
}
|
|
1065
1065
|
end
|
|
1066
1066
|
|
|
@@ -1241,8 +1241,7 @@ module Parse
|
|
|
1241
1241
|
# for tests or internal mongo-direct bulk writes). A class may flip
|
|
1242
1242
|
# the per-class default with `vector_visibility :public`; an explicit
|
|
1243
1243
|
# `include_vectors:` in the call always wins over the class default.
|
|
1244
|
-
include_vectors =
|
|
1245
|
-
if opts.key?(:include_vectors)
|
|
1244
|
+
include_vectors = if opts.key?(:include_vectors)
|
|
1246
1245
|
opts[:include_vectors] == true
|
|
1247
1246
|
else
|
|
1248
1247
|
self.class.respond_to?(:vectors_public_by_default?) && self.class.vectors_public_by_default?
|
|
@@ -1351,6 +1350,12 @@ module Parse
|
|
|
1351
1350
|
# the trust signal here.
|
|
1352
1351
|
trusted = @_trusted_init == true
|
|
1353
1352
|
@_trusted_init = nil
|
|
1353
|
+
input_hash = opts.is_a?(Hash) ? opts : nil
|
|
1354
|
+
input_had_acl = input_hash && %w[ACL acl].any? do |key|
|
|
1355
|
+
input_hash.key?(key) || input_hash.key?(key.to_sym)
|
|
1356
|
+
end
|
|
1357
|
+
input_had_id = input_hash && (input_hash.key?(Parse::Model::OBJECT_ID) || input_hash.key?(:objectId) ||
|
|
1358
|
+
input_hash.key?(:id) || input_hash.key?(Parse::Model::ID))
|
|
1354
1359
|
acl_owner_override = nil
|
|
1355
1360
|
if opts.is_a?(String) #then it's the objectId
|
|
1356
1361
|
@id = opts.to_s
|
|
@@ -1398,6 +1403,31 @@ module Parse
|
|
|
1398
1403
|
@_acl_pristine = !acl_was_user_supplied
|
|
1399
1404
|
@_acl_owner_override = acl_owner_override
|
|
1400
1405
|
|
|
1406
|
+
# Record where our ACL knowledge came from. `acl.nil?` alone is not
|
|
1407
|
+
# enough: it can mean a genuinely ACL-less (therefore public) server
|
|
1408
|
+
# row, but it also describes an id-only pointer or a selective response
|
|
1409
|
+
# that never fetched ACL. Conversely, a custom class may stamp its local
|
|
1410
|
+
# default ACL while hydrating a full server row whose ACL was absent.
|
|
1411
|
+
# Access inspection uses this evidence to distinguish those cases.
|
|
1412
|
+
@_authorization_acl_state = if trusted
|
|
1413
|
+
if has_selective_keys? && !input_had_acl
|
|
1414
|
+
:unknown
|
|
1415
|
+
elsif input_had_acl
|
|
1416
|
+
self.acl.nil? ? :absent : :present
|
|
1417
|
+
else
|
|
1418
|
+
:absent
|
|
1419
|
+
end
|
|
1420
|
+
elsif opts.is_a?(String) || input_had_id
|
|
1421
|
+
:unknown
|
|
1422
|
+
elsif input_had_acl
|
|
1423
|
+
self.acl.nil? ? :unknown : :local
|
|
1424
|
+
elsif self.class.builtin_acl_default_active?
|
|
1425
|
+
:unknown
|
|
1426
|
+
else
|
|
1427
|
+
:local
|
|
1428
|
+
end
|
|
1429
|
+
@_authorization_acl_tracking_ready = true
|
|
1430
|
+
|
|
1401
1431
|
# One-time per-class permissive-default warning. Fires only when the
|
|
1402
1432
|
# effective policy is :public or :owner_else_public.
|
|
1403
1433
|
self.class._warn_permissive_acl_default_once
|
|
@@ -1617,6 +1647,39 @@ module Parse
|
|
|
1617
1647
|
@_fetched_keys.include?(key) || (remote_key && @_fetched_keys.include?(remote_key))
|
|
1618
1648
|
end
|
|
1619
1649
|
|
|
1650
|
+
# Describes whether the in-memory ACL is authoritative enough for local
|
|
1651
|
+
# access inspection. `:absent` means a full trusted server response omitted
|
|
1652
|
+
# ACL (Parse Server's public default); `:present` and `:local` have a usable
|
|
1653
|
+
# ACL value; `:unknown` covers pointers and incomplete hydration.
|
|
1654
|
+
#
|
|
1655
|
+
# @return [Symbol] `:present`, `:absent`, `:local`, or `:unknown`.
|
|
1656
|
+
# @api private
|
|
1657
|
+
def authorization_acl_state
|
|
1658
|
+
@_authorization_acl_state || :unknown
|
|
1659
|
+
end
|
|
1660
|
+
|
|
1661
|
+
# Update ACL evidence after `fetch!` applies a server response to an
|
|
1662
|
+
# existing instance. Save/update responses are deliberately excluded: they
|
|
1663
|
+
# are deltas and omission there says nothing about the stored ACL.
|
|
1664
|
+
# @param attributes [Hash] trusted server response.
|
|
1665
|
+
# @param partial [Boolean] whether a selective key fetch produced it.
|
|
1666
|
+
# @api private
|
|
1667
|
+
def record_authorization_hydration!(attributes, partial: false)
|
|
1668
|
+
return unless attributes.is_a?(Hash)
|
|
1669
|
+
has_acl = %w[ACL acl].any? do |key|
|
|
1670
|
+
attributes.key?(key) || attributes.key?(key.to_sym)
|
|
1671
|
+
end
|
|
1672
|
+
@_authorization_acl_state = if has_acl
|
|
1673
|
+
self.acl.nil? ? :absent : :present
|
|
1674
|
+
elsif partial
|
|
1675
|
+
:unknown
|
|
1676
|
+
else
|
|
1677
|
+
:absent
|
|
1678
|
+
end
|
|
1679
|
+
@_authorization_acl_tracking_ready = true
|
|
1680
|
+
@_authorization_acl_state
|
|
1681
|
+
end
|
|
1682
|
+
|
|
1620
1683
|
# Returns the nested fetched keys map for building nested objects.
|
|
1621
1684
|
# @return [Hash] map of field names to their fetched keys
|
|
1622
1685
|
def nested_fetched_keys
|
|
@@ -1884,48 +1947,48 @@ module Parse
|
|
|
1884
1947
|
owner = @_acl_owner_override if defined?(@_acl_owner_override)
|
|
1885
1948
|
if owner.nil? && (field = self.class.acl_owner_field)
|
|
1886
1949
|
owner = if field == :self
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1950
|
+
# Self-referential ownership (Parse::User only — enforced at
|
|
1951
|
+
# declaration time). Pre-generate a Parse-compatible objectId
|
|
1952
|
+
# client-side so the ACL grant can reference the record's own
|
|
1953
|
+
# id in the same POST body that creates it. Skipped when the id
|
|
1954
|
+
# is already set (e.g. when re-saving an existing user, or when
|
|
1955
|
+
# parse_reference precompute already ran).
|
|
1956
|
+
@id = Parse::Core::ParseReference.generate_object_id if @id.blank?
|
|
1957
|
+
@id
|
|
1958
|
+
elsif respond_to?(field)
|
|
1959
|
+
send(field)
|
|
1960
|
+
end
|
|
1898
1961
|
end
|
|
1899
1962
|
owner_id = _resolve_acl_owner_id(owner)
|
|
1900
1963
|
|
|
1901
1964
|
target_acl = case policy
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1965
|
+
when :public
|
|
1966
|
+
Parse::ACL.everyone(true, true)
|
|
1967
|
+
when :public_read
|
|
1968
|
+
Parse::ACL.everyone(true, false)
|
|
1969
|
+
when :private
|
|
1970
|
+
Parse::ACL.private
|
|
1971
|
+
when :owner_else_public
|
|
1972
|
+
if owner_id
|
|
1973
|
+
acl = Parse::ACL.new
|
|
1974
|
+
acl.apply(owner_id, true, true)
|
|
1975
|
+
acl
|
|
1976
|
+
else
|
|
1977
|
+
Parse::ACL.everyone(true, true)
|
|
1978
|
+
end
|
|
1979
|
+
when :owner_else_private
|
|
1980
|
+
if owner_id
|
|
1981
|
+
acl = Parse::ACL.new
|
|
1982
|
+
acl.apply(owner_id, true, true)
|
|
1983
|
+
acl
|
|
1984
|
+
else
|
|
1985
|
+
Parse::ACL.private
|
|
1986
|
+
end
|
|
1987
|
+
when :owner_but_public_read
|
|
1988
|
+
acl = Parse::ACL.everyone(true, false)
|
|
1989
|
+
acl.apply(owner_id, true, true) if owner_id
|
|
1990
|
+
acl
|
|
1991
|
+
end
|
|
1929
1992
|
|
|
1930
1993
|
# Only re-stamp if the resolved ACL differs from the init-time stamp;
|
|
1931
1994
|
# this avoids an unnecessary dirty mark on the acl field for `:public`
|
|
@@ -1985,6 +2048,9 @@ module Parse
|
|
|
1985
2048
|
@_acl_snapshot_before_change = @acl ? Parse::ACL.new(@acl.as_json) : Parse::ACL.new
|
|
1986
2049
|
end
|
|
1987
2050
|
@_acl_pristine = false if defined?(@_acl_pristine)
|
|
2051
|
+
if defined?(@_authorization_acl_tracking_ready) && @_authorization_acl_tracking_ready
|
|
2052
|
+
@_authorization_acl_state = :local
|
|
2053
|
+
end
|
|
1988
2054
|
super
|
|
1989
2055
|
end
|
|
1990
2056
|
|
|
@@ -2156,16 +2222,16 @@ class Array
|
|
|
2156
2222
|
next m if m.is_a?(Parse::Pointer)
|
|
2157
2223
|
if m.is_a?(Hash)
|
|
2158
2224
|
resolved = if className
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2225
|
+
# Caller knows the type; warn on mismatch but always
|
|
2226
|
+
# use the declared className.
|
|
2227
|
+
incoming = m[f] || m[:className]
|
|
2228
|
+
if incoming && !Parse::Model.same_parse_class?(incoming, className)
|
|
2229
|
+
warn "[Parse::Array#parse_objects] expected className=#{className.inspect}, ignoring incoming className=#{incoming.inspect}"
|
|
2230
|
+
end
|
|
2231
|
+
className
|
|
2232
|
+
else
|
|
2233
|
+
m[f] || m[:className]
|
|
2234
|
+
end
|
|
2169
2235
|
next Parse::Object.build(m, resolved) if resolved
|
|
2170
2236
|
end
|
|
2171
2237
|
nil
|
data/lib/parse/model/pointer.rb
CHANGED
|
@@ -118,8 +118,10 @@ module Parse
|
|
|
118
118
|
# nil unless the instance came from the corresponding search path.
|
|
119
119
|
# @return [Float, nil]
|
|
120
120
|
def vector_score; @_vector_score; end
|
|
121
|
+
|
|
121
122
|
# @return [Float, nil]
|
|
122
123
|
def search_score; @_search_score; end
|
|
124
|
+
|
|
123
125
|
# @return [Hash, nil]
|
|
124
126
|
def search_highlights; @_search_highlights; end
|
|
125
127
|
|
data/lib/parse/model/polygon.rb
CHANGED
|
@@ -99,8 +99,7 @@ module Parse
|
|
|
99
99
|
end
|
|
100
100
|
outer = rings.first
|
|
101
101
|
pairs = outer.map do |(lng, lat)|
|
|
102
|
-
raise ArgumentError, "[Parse::Polygon] GeoJSON ring entries must be [lng, lat] numeric pairs."
|
|
103
|
-
unless lng.is_a?(Numeric) && lat.is_a?(Numeric)
|
|
102
|
+
raise ArgumentError, "[Parse::Polygon] GeoJSON ring entries must be [lng, lat] numeric pairs." unless lng.is_a?(Numeric) && lat.is_a?(Numeric)
|
|
104
103
|
[lat.to_f, lng.to_f]
|
|
105
104
|
end
|
|
106
105
|
new(pairs)
|
|
@@ -120,8 +119,7 @@ module Parse
|
|
|
120
119
|
# - a Hash with a `coordinates` key (the Parse REST wire shape)
|
|
121
120
|
# - another {Parse::Polygon}
|
|
122
121
|
def coordinates=(value)
|
|
123
|
-
coords =
|
|
124
|
-
case value
|
|
122
|
+
coords = case value
|
|
125
123
|
when Parse::Polygon
|
|
126
124
|
# Duplicate so external mutation of the source doesn't leak in.
|
|
127
125
|
value.coordinates.map { |pair| pair.dup }
|
|
@@ -270,8 +268,7 @@ module Parse
|
|
|
270
268
|
# @param point [Parse::GeoPoint, Array<Numeric>] the point to test.
|
|
271
269
|
# @return [Boolean]
|
|
272
270
|
def contains_point?(point)
|
|
273
|
-
lat, lng =
|
|
274
|
-
case point
|
|
271
|
+
lat, lng = case point
|
|
275
272
|
when Parse::GeoPoint then [point.latitude, point.longitude]
|
|
276
273
|
when Array then [point[0].to_f, point[1].to_f]
|
|
277
274
|
else
|
data/lib/parse/model/push.rb
CHANGED
|
@@ -963,11 +963,11 @@ module Parse
|
|
|
963
963
|
if device_type.present? && !SUPPORTED_PUSH_DEVICE_TYPES.include?(device_type)
|
|
964
964
|
if UNSUPPORTED_PUSH_DEVICE_TYPES.include?(device_type)
|
|
965
965
|
warn "[Parse::Push] Warning: device_type '#{device_type}' may not be supported for push notifications. " \
|
|
966
|
-
"Supported types: #{SUPPORTED_PUSH_DEVICE_TYPES.join(
|
|
966
|
+
"Supported types: #{SUPPORTED_PUSH_DEVICE_TYPES.join(", ")}"
|
|
967
967
|
else
|
|
968
968
|
warn "[Parse::Push] Warning: unknown device_type '#{device_type}' for installation #{installation_id}. " \
|
|
969
969
|
"This device type may not receive push notifications. " \
|
|
970
|
-
"Supported types: #{SUPPORTED_PUSH_DEVICE_TYPES.join(
|
|
970
|
+
"Supported types: #{SUPPORTED_PUSH_DEVICE_TYPES.join(", ")}"
|
|
971
971
|
end
|
|
972
972
|
end
|
|
973
973
|
end
|
data/lib/parse/model/vector.rb
CHANGED
|
@@ -57,6 +57,7 @@ module Parse
|
|
|
57
57
|
def dimensions
|
|
58
58
|
@values.length
|
|
59
59
|
end
|
|
60
|
+
|
|
60
61
|
alias_method :length, :dimensions
|
|
61
62
|
alias_method :size, :dimensions
|
|
62
63
|
|
|
@@ -84,10 +85,11 @@ module Parse
|
|
|
84
85
|
def ==(other)
|
|
85
86
|
case other
|
|
86
87
|
when Parse::Vector then @values == other.values
|
|
87
|
-
when Array
|
|
88
|
+
when Array then @values == other
|
|
88
89
|
else false
|
|
89
90
|
end
|
|
90
91
|
end
|
|
92
|
+
|
|
91
93
|
alias_method :eql?, :==
|
|
92
94
|
|
|
93
95
|
def hash
|