parse-stack-next 5.7.6 → 5.8.1
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 +1148 -0
- data/README.md +39 -23
- data/docs/TEST_SERVER.md +2 -2
- data/docs/acl_clp_guide.md +7 -0
- data/docs/atlas_vector_search_guide.md +181 -13
- data/docs/caching.md +50 -0
- data/docs/client_sdk_guide.md +11 -0
- data/docs/mcp_guide.md +332 -9
- data/docs/mongodb_direct_guide.md +27 -0
- data/docs/usage_guide.md +38 -0
- data/docs/webhooks_guide.md +126 -17
- data/lib/parse/acl_scope.rb +230 -50
- data/lib/parse/agent/approval_gate.rb +0 -0
- data/lib/parse/agent/constraint_translator.rb +42 -15
- data/lib/parse/agent/describe.rb +11 -3
- data/lib/parse/agent/field_names.rb +53 -0
- data/lib/parse/agent/field_policy.rb +74 -0
- data/lib/parse/agent/mcp_deployments.rb +426 -0
- data/lib/parse/agent/mcp_dispatcher.rb +8 -1
- data/lib/parse/agent/mcp_rack_app.rb +424 -45
- data/lib/parse/agent/mcp_server.rb +23 -1
- data/lib/parse/agent/mcp_subscriptions.rb +124 -6
- data/lib/parse/agent/metadata_registry.rb +67 -8
- data/lib/parse/agent/prompt_hardening.rb +9 -3
- data/lib/parse/agent/tools.rb +378 -29
- data/lib/parse/agent.rb +298 -21
- data/lib/parse/api/batch.rb +10 -1
- data/lib/parse/api/schema.rb +23 -4
- data/lib/parse/api/sessions.rb +6 -2
- data/lib/parse/api/users.rb +88 -14
- data/lib/parse/atlas_search/index_manager.rb +5 -1
- data/lib/parse/atlas_search/protected_paths.rb +236 -0
- data/lib/parse/atlas_search.rb +144 -24
- data/lib/parse/authorization.rb +289 -4
- data/lib/parse/cache/sub_cache.rb +26 -0
- data/lib/parse/client/authentication.rb +19 -1
- data/lib/parse/client/batch.rb +264 -35
- data/lib/parse/client/body_builder.rb +21 -0
- data/lib/parse/client/caching.rb +371 -27
- data/lib/parse/client/request.rb +26 -14
- data/lib/parse/client/response.rb +49 -6
- data/lib/parse/client.rb +227 -40
- data/lib/parse/clp_scope.rb +317 -25
- data/lib/parse/console.rb +2 -2
- data/lib/parse/embeddings/voyage.rb +181 -17
- data/lib/parse/graphql/type_generator.rb +3 -0
- data/lib/parse/live_query/client.rb +72 -2
- data/lib/parse/lock_backend.rb +4 -3
- data/lib/parse/model/acl.rb +119 -21
- data/lib/parse/model/associations/belongs_to.rb +25 -3
- data/lib/parse/model/associations/collection_proxy.rb +146 -17
- data/lib/parse/model/associations/has_many.rb +49 -9
- data/lib/parse/model/associations/has_one.rb +3 -1
- data/lib/parse/model/associations/pointer_collection_proxy.rb +145 -17
- data/lib/parse/model/associations/relation_collection_proxy.rb +229 -38
- data/lib/parse/model/bytes.rb +13 -5
- data/lib/parse/model/classes/role.rb +95 -0
- data/lib/parse/model/classes/session.rb +304 -1
- data/lib/parse/model/classes/user.rb +104 -8
- data/lib/parse/model/core/actions.rb +287 -70
- data/lib/parse/model/core/builder.rb +100 -8
- data/lib/parse/model/core/create_lock.rb +27 -2
- data/lib/parse/model/core/describe.rb +2 -0
- data/lib/parse/model/core/fetching.rb +50 -5
- data/lib/parse/model/core/field_guards.rb +16 -8
- data/lib/parse/model/core/pluralized_aliases.rb +8 -4
- data/lib/parse/model/core/properties.rb +488 -39
- data/lib/parse/model/core/querying.rb +7 -0
- data/lib/parse/model/core/schema.rb +5 -3
- data/lib/parse/model/core/search_indexing.rb +63 -0
- data/lib/parse/model/core/vector_searchable.rb +35 -6
- data/lib/parse/model/file.rb +9 -2
- data/lib/parse/model/geopoint.rb +61 -13
- data/lib/parse/model/model.rb +160 -9
- data/lib/parse/model/object.rb +317 -18
- data/lib/parse/model/phone.rb +54 -5
- data/lib/parse/model/pointer.rb +40 -6
- data/lib/parse/model/push.rb +11 -1
- data/lib/parse/mongodb.rb +189 -61
- data/lib/parse/pipeline_security.rb +415 -26
- data/lib/parse/query/constraint.rb +31 -1
- data/lib/parse/query/constraints.rb +89 -59
- data/lib/parse/query/cursor.rb +3 -1
- data/lib/parse/query/operation.rb +62 -8
- data/lib/parse/query/ordering.rb +34 -6
- data/lib/parse/query.rb +1871 -167
- data/lib/parse/retrieval/agent_tool.rb +225 -8
- data/lib/parse/retrieval/benchmark.rb +149 -0
- data/lib/parse/retrieval/profiles.rb +320 -0
- data/lib/parse/retrieval/retriever.rb +10 -1
- data/lib/parse/retrieval.rb +2 -0
- data/lib/parse/schema/search_index_migrator.rb +23 -5
- data/lib/parse/schema.rb +74 -18
- data/lib/parse/stack/tasks.rb +6 -4
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +91 -14
- data/lib/parse/two_factor_auth/user_extension.rb +14 -2
- data/lib/parse/two_factor_auth.rb +11 -0
- data/lib/parse/vector_search/hybrid.rb +110 -33
- data/lib/parse/vector_search/index_definition.rb +237 -0
- data/lib/parse/vector_search.rb +259 -23
- data/lib/parse/webhooks/payload.rb +136 -6
- data/lib/parse/webhooks/replay_protection.rb +58 -20
- data/lib/parse/webhooks.rb +715 -40
- metadata +8 -1
data/lib/parse/mongodb.rb
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
require "date"
|
|
5
5
|
require "set"
|
|
6
6
|
require "time"
|
|
7
|
+
require "erb"
|
|
7
8
|
require_relative "pipeline_security"
|
|
8
9
|
require_relative "clp_scope"
|
|
9
10
|
require_relative "acl_scope"
|
|
@@ -1165,6 +1166,9 @@ module Parse
|
|
|
1165
1166
|
# in {Parse::PipelineSecurity::DENIED_OPERATORS}.
|
|
1166
1167
|
DENIED_OPERATORS = Parse::PipelineSecurity::DENIED_OPERATORS
|
|
1167
1168
|
|
|
1169
|
+
# @!visibility private
|
|
1170
|
+
EMPTY_COLUMN_SET = Set.new.freeze
|
|
1171
|
+
|
|
1168
1172
|
# @!visibility private
|
|
1169
1173
|
# Default BFS depth for role-graph expansion. Real-world role graphs
|
|
1170
1174
|
# are 2-4 deep; 10 leaves headroom for unusual hierarchies without
|
|
@@ -1477,6 +1481,15 @@ module Parse
|
|
|
1477
1481
|
"They are mutually exclusive."
|
|
1478
1482
|
end
|
|
1479
1483
|
|
|
1484
|
+
if master == true && Parse::ACLScope.master_key_suppressed?
|
|
1485
|
+
# The role graph has no public mode to fall back to, so a master
|
|
1486
|
+
# call inside `Parse.without_master_key` is refused outright.
|
|
1487
|
+
raise Parse::ACLScope::ACLRequired,
|
|
1488
|
+
"Parse::MongoDB.#{method_name}: master: true is dropped inside " \
|
|
1489
|
+
"Parse.without_master_key. Pass `as: <Parse::User|Parse::Pointer>`, " \
|
|
1490
|
+
"or run the call inside Parse.with_master_key."
|
|
1491
|
+
end
|
|
1492
|
+
|
|
1480
1493
|
if master == true
|
|
1481
1494
|
return Parse::ACLScope::Resolution.new(
|
|
1482
1495
|
mode: :master, permission_strings: nil, user_id: nil, session: nil,
|
|
@@ -1778,6 +1791,10 @@ module Parse
|
|
|
1778
1791
|
# database with this application's permission strings.
|
|
1779
1792
|
verify_client!(resolution.client)
|
|
1780
1793
|
payload[:scope] = __scope_label(resolution)
|
|
1794
|
+
# `master: true` inside Parse.without_master_key, downgraded to the
|
|
1795
|
+
# public scope. Lets a subscriber explain a master-key caller that
|
|
1796
|
+
# sees only public rows inside the block.
|
|
1797
|
+
payload[:master_dropped] = true if resolution.respond_to?(:master_dropped?) && resolution.master_dropped?
|
|
1781
1798
|
|
|
1782
1799
|
# Validate BEFORE rewrite so the security denylist is applied to the
|
|
1783
1800
|
# caller's original pipeline (which an attacker controls), not to
|
|
@@ -1803,6 +1820,12 @@ module Parse
|
|
|
1803
1820
|
pipeline, class_name: collection_name, enabled: rewrite_lookups,
|
|
1804
1821
|
)
|
|
1805
1822
|
|
|
1823
|
+
# Decide, from the caller's own stages, whether a returned row's
|
|
1824
|
+
# `_id` is still the stored one. The `_User` self exemption below
|
|
1825
|
+
# trusts the output `_id`, so it is granted only when no caller
|
|
1826
|
+
# stage could have rewritten it; otherwise the field is stripped.
|
|
1827
|
+
identity_preserved = Parse::PipelineSecurity.identity_preserving?(pipeline)
|
|
1828
|
+
|
|
1806
1829
|
# Three-layer ACL simulation on the mongo-direct path:
|
|
1807
1830
|
#
|
|
1808
1831
|
# 1. Top-level $match: filter the queried collection's rows by
|
|
@@ -1824,42 +1847,38 @@ module Parse
|
|
|
1824
1847
|
# pipeline above; the injected stages reference `_rperm` but
|
|
1825
1848
|
# are SDK-generated (not attacker-controlled), so no
|
|
1826
1849
|
# re-validation is needed before they're handed to MongoDB.
|
|
1827
|
-
if (acl_stage = Parse::ACLScope.match_stage_for(resolution))
|
|
1828
|
-
pipeline = prepend_or_fold_acl_match(pipeline, acl_stage)
|
|
1829
|
-
end
|
|
1830
|
-
pipeline = Parse::ACLScope.rewrite_pipeline(pipeline, resolution)
|
|
1831
|
-
|
|
1832
1850
|
# Class-Level Permissions boundary check. Parse Server's REST
|
|
1833
1851
|
# aggregate endpoint runs master-key-only and does NOT enforce
|
|
1834
1852
|
# CLP; the mongo-direct path bypasses Parse Server entirely so
|
|
1835
|
-
# the SDK is the only enforcement layer.
|
|
1836
|
-
#
|
|
1837
|
-
#
|
|
1853
|
+
# the SDK is the only enforcement layer. Evaluate the `find` CLP
|
|
1854
|
+
# with Parse Server's branch semantics: a public / user / role
|
|
1855
|
+
# grant permits every row, otherwise a pointerFields or
|
|
1856
|
+
# readUserFields grant permits only the rows whose named pointer
|
|
1857
|
+
# references the requesting user. Raises when the scope cannot
|
|
1858
|
+
# find at all. Master-key (resolution.master?) bypasses.
|
|
1838
1859
|
perms_for_clp = resolution&.permission_strings
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
collection_name, :find,
|
|
1843
|
-
"CLP refuses find on '#{collection_name}' for the current scope.",
|
|
1844
|
-
)
|
|
1845
|
-
end
|
|
1846
|
-
end
|
|
1860
|
+
pointer_fields = Parse::CLPScope.row_constraint_for!(
|
|
1861
|
+
collection_name, :find, resolution, client: clp_client_for(resolution),
|
|
1862
|
+
)
|
|
1847
1863
|
|
|
1848
|
-
#
|
|
1849
|
-
#
|
|
1850
|
-
#
|
|
1851
|
-
#
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1864
|
+
# The pointer-permission constraint is applied IN the pipeline,
|
|
1865
|
+
# folded into the leading ACL `$match`, so it runs before any
|
|
1866
|
+
# caller `$skip` / `$limit` / `$count` / `$group`. Filtering the
|
|
1867
|
+
# fetched rows afterwards made `count_direct` return 0 (the
|
|
1868
|
+
# `$count` row has no pointer column) and returned short or
|
|
1869
|
+
# empty pages.
|
|
1870
|
+
front_predicates = []
|
|
1871
|
+
if (acl_stage = Parse::ACLScope.match_stage_for(resolution))
|
|
1872
|
+
front_predicates << (acl_stage["$match"] || acl_stage[:$match])
|
|
1873
|
+
end
|
|
1874
|
+
if pointer_fields
|
|
1875
|
+
front_predicates << Parse::CLPScope.pointer_fields_predicate(pointer_fields, resolution.user_id)
|
|
1876
|
+
end
|
|
1877
|
+
unless front_predicates.empty?
|
|
1878
|
+
front = front_predicates.size == 1 ? front_predicates.first : { "$and" => front_predicates }
|
|
1879
|
+
pipeline = prepend_or_fold_acl_match(pipeline, { "$match" => front })
|
|
1862
1880
|
end
|
|
1881
|
+
pipeline = Parse::ACLScope.rewrite_pipeline(pipeline, resolution, class_name: collection_name)
|
|
1863
1882
|
|
|
1864
1883
|
agg_opts = {}
|
|
1865
1884
|
agg_opts[:max_time_ms] = max_time_ms if max_time_ms
|
|
@@ -1879,25 +1898,25 @@ module Parse
|
|
|
1879
1898
|
results = with_query_killed_retry(collection_name) { coll.aggregate(pipeline, agg_opts).to_a }
|
|
1880
1899
|
Parse::ACLScope.redact_results!(results, resolution)
|
|
1881
1900
|
|
|
1882
|
-
#
|
|
1883
|
-
#
|
|
1884
|
-
#
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
#
|
|
1892
|
-
# session's claim composition and walk-delete from every
|
|
1893
|
-
# row + embedded sub-document. Top-level $project would also
|
|
1894
|
-
# work but doesn't reach inside `$lookup`-included sub-docs,
|
|
1895
|
-
# so the post-walker is the defense-in-depth layer.
|
|
1901
|
+
# Protected fields stripping, top-level keys only (Parse Server
|
|
1902
|
+
# deletes `object[key]` and does not descend). The queried class's
|
|
1903
|
+
# set applies to each row; every `$lookup` in the executed pipeline
|
|
1904
|
+
# pulls rows of another class under its `as` key, and those rows get
|
|
1905
|
+
# THAT class's set, the same as Parse Server's include sub-query
|
|
1906
|
+
# does. `_User` rows that are the requesting user keep their own
|
|
1907
|
+
# protected fields, as on REST, but only when that identity is
|
|
1908
|
+
# proven: the caller's stages preserve `_id` (root rows), or the
|
|
1909
|
+
# join's head strip already decided ownership on the stored `_id`
|
|
1910
|
+
# (joined rows, see {Parse::ACLScope.rewrite_pipeline}).
|
|
1896
1911
|
unless resolution.nil? || resolution.master?
|
|
1897
1912
|
strip_set = Parse::CLPScope.protected_fields_for(
|
|
1898
|
-
collection_name, perms_for_clp,
|
|
1913
|
+
collection_name, perms_for_clp, client: clp_client_for(resolution),
|
|
1899
1914
|
)
|
|
1900
|
-
Parse::CLPScope.redact_protected_fields!(
|
|
1915
|
+
Parse::CLPScope.redact_protected_fields!(
|
|
1916
|
+
results, strip_set, class_name: collection_name,
|
|
1917
|
+
user_id: identity_preserved ? resolution.user_id : nil,
|
|
1918
|
+
) if strip_set.any?
|
|
1919
|
+
redact_joined_protected_fields!(results, pipeline, resolution)
|
|
1901
1920
|
|
|
1902
1921
|
# Process-level floor: recursively strip Parse-internal credential
|
|
1903
1922
|
# columns (_hashed_password, _session_token, _auth_data_*, _rperm,
|
|
@@ -1920,6 +1939,56 @@ module Parse
|
|
|
1920
1939
|
raise
|
|
1921
1940
|
end
|
|
1922
1941
|
|
|
1942
|
+
# The client whose schema decides CLP for a resolution. A user-scoped
|
|
1943
|
+
# client (from `become` / `session_client`) holds no master key and
|
|
1944
|
+
# cannot read `/schemas`, so it falls back to the default client (nil
|
|
1945
|
+
# here), which is what every CLP lookup used before resolutions
|
|
1946
|
+
# carried a client.
|
|
1947
|
+
# @!visibility private
|
|
1948
|
+
def clp_client_for(resolution)
|
|
1949
|
+
c = Parse::ACLScope.client_of(resolution)
|
|
1950
|
+
return nil if c.nil?
|
|
1951
|
+
return nil unless c.respond_to?(:master_key) && !c.master_key.to_s.empty?
|
|
1952
|
+
c
|
|
1953
|
+
end
|
|
1954
|
+
|
|
1955
|
+
# Strip each joined class's protectedFields from the rows a top-level
|
|
1956
|
+
# `$lookup` pulled in. Mirrors Parse Server's include, which fetches the
|
|
1957
|
+
# included objects with a sub-query on their own class and therefore
|
|
1958
|
+
# applies that class's protectedFields (plus the server default
|
|
1959
|
+
# `_User.email` protection) to them.
|
|
1960
|
+
# @!visibility private
|
|
1961
|
+
def redact_joined_protected_fields!(results, pipeline, resolution)
|
|
1962
|
+
return if results.nil? || results.empty? || !pipeline.is_a?(Array)
|
|
1963
|
+
perms = resolution.permission_strings
|
|
1964
|
+
return if perms.nil?
|
|
1965
|
+
# The self exemption is safe only when the join's sub-pipeline began
|
|
1966
|
+
# with the strip stage, which decided ownership on the stored `_id`.
|
|
1967
|
+
self_id = Parse::ACLScope.rewrites_joins?(resolution) ? resolution.user_id : nil
|
|
1968
|
+
pipeline.each do |stage|
|
|
1969
|
+
next unless stage.is_a?(Hash)
|
|
1970
|
+
spec = stage["$lookup"] || stage[:$lookup]
|
|
1971
|
+
next unless spec.is_a?(Hash)
|
|
1972
|
+
from = spec["from"] || spec[:from]
|
|
1973
|
+
as = spec["as"] || spec[:as]
|
|
1974
|
+
next if from.to_s.empty? || as.to_s.empty?
|
|
1975
|
+
set = Parse::CLPScope.protected_fields_for(from.to_s, perms, client: clp_client_for(resolution))
|
|
1976
|
+
next if set.empty?
|
|
1977
|
+
path = as.to_s.split(".")
|
|
1978
|
+
results.each do |row|
|
|
1979
|
+
value = path.reduce(row) { |node, seg| node.is_a?(Hash) ? (node[seg] || node[seg.to_sym]) : nil }
|
|
1980
|
+
docs = case value
|
|
1981
|
+
when Hash then [value]
|
|
1982
|
+
when Array then value.select { |v| v.is_a?(Hash) }
|
|
1983
|
+
else []
|
|
1984
|
+
end
|
|
1985
|
+
next if docs.empty?
|
|
1986
|
+
Parse::CLPScope.redact_protected_fields!(docs, set, class_name: from.to_s, user_id: self_id)
|
|
1987
|
+
end
|
|
1988
|
+
end
|
|
1989
|
+
nil
|
|
1990
|
+
end
|
|
1991
|
+
|
|
1923
1992
|
# Inject the scoped ACL `$match` at the front of a pipeline — UNLESS
|
|
1924
1993
|
# the first stage is `$geoNear`, which MongoDB requires to be
|
|
1925
1994
|
# pipeline stage 0. In that case fold the ACL predicate into
|
|
@@ -2247,7 +2316,12 @@ module Parse
|
|
|
2247
2316
|
"the caller is authorized. Callers without master scope (e.g. agent " \
|
|
2248
2317
|
"tools, request handlers) must not invoke this method."
|
|
2249
2318
|
end
|
|
2250
|
-
|
|
2319
|
+
# Index metadata, not rows: keep master mode inside
|
|
2320
|
+
# `Parse.without_master_key`, which only governs row access. The
|
|
2321
|
+
# sentinel elevates scope resolution only; it leaves the block's
|
|
2322
|
+
# fiber state alone, so notification subscribers still see it.
|
|
2323
|
+
results = aggregate(collection_name, [{ "$indexStats" => {} }],
|
|
2324
|
+
master: Parse::ACLScope::METADATA_MASTER)
|
|
2251
2325
|
results.each_with_object({}) do |row, h|
|
|
2252
2326
|
name = row["name"] || row[:name]
|
|
2253
2327
|
next unless name
|
|
@@ -2279,6 +2353,8 @@ module Parse
|
|
|
2279
2353
|
return nil unless doc.is_a?(Hash)
|
|
2280
2354
|
|
|
2281
2355
|
result = {}
|
|
2356
|
+
included = {}
|
|
2357
|
+
file_fields = file_columns_for(class_name)
|
|
2282
2358
|
|
|
2283
2359
|
doc.each do |key, value|
|
|
2284
2360
|
key_str = key.to_s
|
|
@@ -2306,18 +2382,10 @@ module Parse
|
|
|
2306
2382
|
# Convert MongoDB ACL format (r/w) to Parse format (read/write)
|
|
2307
2383
|
result["ACL"] = convert_acl_to_parse(value)
|
|
2308
2384
|
when /^_included_(.+)$/
|
|
2309
|
-
# Included/resolved pointer field from $lookup
|
|
2310
|
-
#
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
# Recursively convert the embedded document to Parse format
|
|
2314
|
-
result[field_name] = convert_document_to_parse(value)
|
|
2315
|
-
elsif value.nil?
|
|
2316
|
-
# Preserve nil for unresolved optional relationships
|
|
2317
|
-
result[field_name] = nil
|
|
2318
|
-
else
|
|
2319
|
-
result[field_name] = value
|
|
2320
|
-
end
|
|
2385
|
+
# Included/resolved pointer field from $lookup. Resolved after
|
|
2386
|
+
# the loop, once the sibling `_p_<field>` pointer (which names
|
|
2387
|
+
# the included object's class) has been seen.
|
|
2388
|
+
included[$1] = value
|
|
2321
2389
|
when /^_include_id_/
|
|
2322
2390
|
# Skip temporary lookup ID fields (used internally for $lookup)
|
|
2323
2391
|
next
|
|
@@ -2330,8 +2398,33 @@ module Parse
|
|
|
2330
2398
|
# Skip other internal fields starting with underscore
|
|
2331
2399
|
next
|
|
2332
2400
|
else
|
|
2333
|
-
# Regular fields - recursively convert nested documents
|
|
2334
|
-
|
|
2401
|
+
# Regular fields - recursively convert nested documents. Parse
|
|
2402
|
+
# Server stores a File column as the bare file name; REST returns
|
|
2403
|
+
# it as a `File` object, so rebuild that shape.
|
|
2404
|
+
result[key_str] = if value.is_a?(String) && file_fields.include?(key_str)
|
|
2405
|
+
file_to_parse(value)
|
|
2406
|
+
else
|
|
2407
|
+
convert_value_to_parse(value)
|
|
2408
|
+
end
|
|
2409
|
+
end
|
|
2410
|
+
end
|
|
2411
|
+
|
|
2412
|
+
included.each do |field_name, value|
|
|
2413
|
+
if value.is_a?(Hash)
|
|
2414
|
+
# REST returns an included object as a full `Object` carrying
|
|
2415
|
+
# its `__type` and `className`. The class comes from the stored
|
|
2416
|
+
# pointer (`Class$objectId`); without it the object decoded as nil.
|
|
2417
|
+
pointer = doc["_p_#{field_name}"] || doc[:"_p_#{field_name}"]
|
|
2418
|
+
target = pointer.is_a?(String) && pointer.include?("$") ? pointer.split("$", 2).first : nil
|
|
2419
|
+
converted = convert_document_to_parse(value, target)
|
|
2420
|
+
converted["__type"] = Parse::Model::TYPE_OBJECT if target
|
|
2421
|
+
result[field_name] = converted
|
|
2422
|
+
elsif value.nil?
|
|
2423
|
+
# The include resolved to nothing: a dangling pointer, or a row
|
|
2424
|
+
# the scope cannot read. REST omits the field in that case.
|
|
2425
|
+
result.delete(field_name)
|
|
2426
|
+
else
|
|
2427
|
+
result[field_name] = value
|
|
2335
2428
|
end
|
|
2336
2429
|
end
|
|
2337
2430
|
|
|
@@ -2341,6 +2434,41 @@ module Parse
|
|
|
2341
2434
|
result
|
|
2342
2435
|
end
|
|
2343
2436
|
|
|
2437
|
+
# Mongo column names of the `:file` properties declared on the model
|
|
2438
|
+
# registered for `class_name`.
|
|
2439
|
+
# @return [Set<String>]
|
|
2440
|
+
# @!visibility private
|
|
2441
|
+
def file_columns_for(class_name)
|
|
2442
|
+
return EMPTY_COLUMN_SET if class_name.nil?
|
|
2443
|
+
klass = Parse::Model.find_class(class_name.to_s) rescue nil
|
|
2444
|
+
return EMPTY_COLUMN_SET unless klass.respond_to?(:fields)
|
|
2445
|
+
map = klass.respond_to?(:field_map) ? klass.field_map : {}
|
|
2446
|
+
klass.fields.each_with_object(Set.new) do |(local, type), set|
|
|
2447
|
+
next unless type == :file
|
|
2448
|
+
set << (map[local] || local).to_s
|
|
2449
|
+
end
|
|
2450
|
+
rescue StandardError
|
|
2451
|
+
EMPTY_COLUMN_SET
|
|
2452
|
+
end
|
|
2453
|
+
|
|
2454
|
+
# Parse Server's REST shape for a stored file name. The URL follows
|
|
2455
|
+
# Parse Server's default files route (`<serverURL>/files/<appId>/<name>`),
|
|
2456
|
+
# used by the GridFS and filesystem adapters and by adapters without
|
|
2457
|
+
# direct access. An adapter with direct access (for example S3 with
|
|
2458
|
+
# `directAccess: true`) serves a different URL; there REST is the
|
|
2459
|
+
# authoritative source.
|
|
2460
|
+
# @!visibility private
|
|
2461
|
+
def file_to_parse(name)
|
|
2462
|
+
out = { "__type" => Parse::Model::TYPE_FILE, "name" => name }
|
|
2463
|
+
client = Parse::Client.client? ? (Parse::Client.client rescue nil) : nil
|
|
2464
|
+
if client && client.respond_to?(:server_url) && client.respond_to?(:application_id) &&
|
|
2465
|
+
!client.server_url.to_s.empty? && !client.application_id.to_s.empty?
|
|
2466
|
+
base = client.server_url.to_s.chomp("/")
|
|
2467
|
+
out["url"] = "#{base}/files/#{client.application_id}/#{ERB::Util.url_encode(name)}"
|
|
2468
|
+
end
|
|
2469
|
+
out
|
|
2470
|
+
end
|
|
2471
|
+
|
|
2344
2472
|
# Convert multiple MongoDB documents to Parse format
|
|
2345
2473
|
# @param docs [Array<Hash>] the MongoDB documents
|
|
2346
2474
|
# @param class_name [String] the Parse class name
|