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.
Files changed (106) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1148 -0
  3. data/README.md +39 -23
  4. data/docs/TEST_SERVER.md +2 -2
  5. data/docs/acl_clp_guide.md +7 -0
  6. data/docs/atlas_vector_search_guide.md +181 -13
  7. data/docs/caching.md +50 -0
  8. data/docs/client_sdk_guide.md +11 -0
  9. data/docs/mcp_guide.md +332 -9
  10. data/docs/mongodb_direct_guide.md +27 -0
  11. data/docs/usage_guide.md +38 -0
  12. data/docs/webhooks_guide.md +126 -17
  13. data/lib/parse/acl_scope.rb +230 -50
  14. data/lib/parse/agent/approval_gate.rb +0 -0
  15. data/lib/parse/agent/constraint_translator.rb +42 -15
  16. data/lib/parse/agent/describe.rb +11 -3
  17. data/lib/parse/agent/field_names.rb +53 -0
  18. data/lib/parse/agent/field_policy.rb +74 -0
  19. data/lib/parse/agent/mcp_deployments.rb +426 -0
  20. data/lib/parse/agent/mcp_dispatcher.rb +8 -1
  21. data/lib/parse/agent/mcp_rack_app.rb +424 -45
  22. data/lib/parse/agent/mcp_server.rb +23 -1
  23. data/lib/parse/agent/mcp_subscriptions.rb +124 -6
  24. data/lib/parse/agent/metadata_registry.rb +67 -8
  25. data/lib/parse/agent/prompt_hardening.rb +9 -3
  26. data/lib/parse/agent/tools.rb +378 -29
  27. data/lib/parse/agent.rb +298 -21
  28. data/lib/parse/api/batch.rb +10 -1
  29. data/lib/parse/api/schema.rb +23 -4
  30. data/lib/parse/api/sessions.rb +6 -2
  31. data/lib/parse/api/users.rb +88 -14
  32. data/lib/parse/atlas_search/index_manager.rb +5 -1
  33. data/lib/parse/atlas_search/protected_paths.rb +236 -0
  34. data/lib/parse/atlas_search.rb +144 -24
  35. data/lib/parse/authorization.rb +289 -4
  36. data/lib/parse/cache/sub_cache.rb +26 -0
  37. data/lib/parse/client/authentication.rb +19 -1
  38. data/lib/parse/client/batch.rb +264 -35
  39. data/lib/parse/client/body_builder.rb +21 -0
  40. data/lib/parse/client/caching.rb +371 -27
  41. data/lib/parse/client/request.rb +26 -14
  42. data/lib/parse/client/response.rb +49 -6
  43. data/lib/parse/client.rb +227 -40
  44. data/lib/parse/clp_scope.rb +317 -25
  45. data/lib/parse/console.rb +2 -2
  46. data/lib/parse/embeddings/voyage.rb +181 -17
  47. data/lib/parse/graphql/type_generator.rb +3 -0
  48. data/lib/parse/live_query/client.rb +72 -2
  49. data/lib/parse/lock_backend.rb +4 -3
  50. data/lib/parse/model/acl.rb +119 -21
  51. data/lib/parse/model/associations/belongs_to.rb +25 -3
  52. data/lib/parse/model/associations/collection_proxy.rb +146 -17
  53. data/lib/parse/model/associations/has_many.rb +49 -9
  54. data/lib/parse/model/associations/has_one.rb +3 -1
  55. data/lib/parse/model/associations/pointer_collection_proxy.rb +145 -17
  56. data/lib/parse/model/associations/relation_collection_proxy.rb +229 -38
  57. data/lib/parse/model/bytes.rb +13 -5
  58. data/lib/parse/model/classes/role.rb +95 -0
  59. data/lib/parse/model/classes/session.rb +304 -1
  60. data/lib/parse/model/classes/user.rb +104 -8
  61. data/lib/parse/model/core/actions.rb +287 -70
  62. data/lib/parse/model/core/builder.rb +100 -8
  63. data/lib/parse/model/core/create_lock.rb +27 -2
  64. data/lib/parse/model/core/describe.rb +2 -0
  65. data/lib/parse/model/core/fetching.rb +50 -5
  66. data/lib/parse/model/core/field_guards.rb +16 -8
  67. data/lib/parse/model/core/pluralized_aliases.rb +8 -4
  68. data/lib/parse/model/core/properties.rb +488 -39
  69. data/lib/parse/model/core/querying.rb +7 -0
  70. data/lib/parse/model/core/schema.rb +5 -3
  71. data/lib/parse/model/core/search_indexing.rb +63 -0
  72. data/lib/parse/model/core/vector_searchable.rb +35 -6
  73. data/lib/parse/model/file.rb +9 -2
  74. data/lib/parse/model/geopoint.rb +61 -13
  75. data/lib/parse/model/model.rb +160 -9
  76. data/lib/parse/model/object.rb +317 -18
  77. data/lib/parse/model/phone.rb +54 -5
  78. data/lib/parse/model/pointer.rb +40 -6
  79. data/lib/parse/model/push.rb +11 -1
  80. data/lib/parse/mongodb.rb +189 -61
  81. data/lib/parse/pipeline_security.rb +415 -26
  82. data/lib/parse/query/constraint.rb +31 -1
  83. data/lib/parse/query/constraints.rb +89 -59
  84. data/lib/parse/query/cursor.rb +3 -1
  85. data/lib/parse/query/operation.rb +62 -8
  86. data/lib/parse/query/ordering.rb +34 -6
  87. data/lib/parse/query.rb +1871 -167
  88. data/lib/parse/retrieval/agent_tool.rb +225 -8
  89. data/lib/parse/retrieval/benchmark.rb +149 -0
  90. data/lib/parse/retrieval/profiles.rb +320 -0
  91. data/lib/parse/retrieval/retriever.rb +10 -1
  92. data/lib/parse/retrieval.rb +2 -0
  93. data/lib/parse/schema/search_index_migrator.rb +23 -5
  94. data/lib/parse/schema.rb +74 -18
  95. data/lib/parse/stack/tasks.rb +6 -4
  96. data/lib/parse/stack/version.rb +1 -1
  97. data/lib/parse/stack.rb +91 -14
  98. data/lib/parse/two_factor_auth/user_extension.rb +14 -2
  99. data/lib/parse/two_factor_auth.rb +11 -0
  100. data/lib/parse/vector_search/hybrid.rb +110 -33
  101. data/lib/parse/vector_search/index_definition.rb +237 -0
  102. data/lib/parse/vector_search.rb +259 -23
  103. data/lib/parse/webhooks/payload.rb +136 -6
  104. data/lib/parse/webhooks/replay_protection.rb +58 -20
  105. data/lib/parse/webhooks.rb +715 -40
  106. 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. Refuse the call when
1836
- # the resolved scope can't `find` on the collection. Master-
1837
- # key (resolution.master? / nil permission_strings) bypasses.
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
- unless resolution.nil? || resolution.master?
1840
- unless Parse::CLPScope.permits?(collection_name, :find, perms_for_clp)
1841
- raise Parse::CLPScope::Denied.new(
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
- # Resolve the pointerFields constraint (if any) BEFORE running
1849
- # the query — we apply the filter post-fetch but want to fail
1850
- # loudly when the scope can't satisfy the constraint at all
1851
- # (acl_role-only / public agents have no user_id to match).
1852
- pointer_fields = nil
1853
- unless resolution.nil? || resolution.master?
1854
- pointer_fields = Parse::CLPScope.pointer_fields_for(collection_name, :find)
1855
- if pointer_fields && resolution.user_id.nil?
1856
- raise Parse::CLPScope::Denied.new(
1857
- collection_name, :find,
1858
- "CLP requires user identity (pointerFields=#{pointer_fields.inspect}) " \
1859
- "but the current scope has no user_id.",
1860
- )
1861
- end
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
- # Post-fetch pointerFields filter: drop rows where none of the
1883
- # named pointer fields references the requesting user. Skipped
1884
- # for master-key and when the CLP has no pointerFields entry.
1885
- if pointer_fields
1886
- results = Parse::CLPScope.filter_by_pointer_fields(
1887
- results, pointer_fields, resolution.user_id,
1888
- )
1889
- end
1890
-
1891
- # Protected fields stripping. Resolve the field set per the
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!(results, strip_set) if strip_set.any?
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
- results = aggregate(collection_name, [{ "$indexStats" => {} }], master: true)
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 - convert embedded document
2310
- # This handles eager loading: _included_artist -> artist (as full object)
2311
- field_name = $1
2312
- if value.is_a?(Hash)
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
- result[key_str] = convert_value_to_parse(value)
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