parse-stack-next 5.7.6 → 5.8.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 +830 -0
- data/README.md +14 -4
- 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/client_sdk_guide.md +11 -0
- data/docs/mcp_guide.md +317 -6
- data/docs/mongodb_direct_guide.md +27 -0
- data/docs/usage_guide.md +38 -0
- data/docs/webhooks_guide.md +74 -17
- data/lib/parse/acl_scope.rb +159 -41
- data/lib/parse/agent/approval_gate.rb +0 -0
- data/lib/parse/agent/constraint_translator.rb +42 -15
- data/lib/parse/agent/describe.rb +3 -1
- 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_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 +93 -1
- 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/protected_paths.rb +236 -0
- data/lib/parse/atlas_search.rb +95 -23
- data/lib/parse/authorization.rb +54 -1
- data/lib/parse/client/batch.rb +231 -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 +201 -38
- data/lib/parse/clp_scope.rb +281 -23
- 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/model/acl.rb +119 -21
- data/lib/parse/model/associations/belongs_to.rb +25 -3
- data/lib/parse/model/associations/collection_proxy.rb +138 -17
- data/lib/parse/model/associations/has_many.rb +38 -9
- data/lib/parse/model/associations/has_one.rb +3 -1
- data/lib/parse/model/associations/pointer_collection_proxy.rb +109 -17
- data/lib/parse/model/associations/relation_collection_proxy.rb +134 -28
- data/lib/parse/model/bytes.rb +13 -5
- data/lib/parse/model/classes/role.rb +72 -0
- data/lib/parse/model/classes/session.rb +43 -0
- data/lib/parse/model/classes/user.rb +78 -3
- data/lib/parse/model/core/actions.rb +269 -67
- 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 +21 -3
- 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 +265 -17
- data/lib/parse/model/phone.rb +54 -5
- data/lib/parse/model/pointer.rb +40 -6
- data/lib/parse/mongodb.rb +170 -60
- data/lib/parse/pipeline_security.rb +415 -26
- data/lib/parse/query/constraint.rb +30 -0
- data/lib/parse/query/constraints.rb +58 -32
- 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 +1100 -134
- 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 +72 -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 +36 -18
- data/lib/parse/vector_search/index_definition.rb +237 -0
- data/lib/parse/vector_search.rb +46 -17
- data/lib/parse/webhooks/payload.rb +93 -6
- data/lib/parse/webhooks/replay_protection.rb +58 -20
- data/lib/parse/webhooks.rb +412 -40
- metadata +8 -1
data/lib/parse/model/object.rb
CHANGED
|
@@ -387,10 +387,21 @@ module Parse
|
|
|
387
387
|
# @return [String] the name of the Parse collection for this model.
|
|
388
388
|
def parse_class(remoteName = nil)
|
|
389
389
|
@parse_class ||= model_name.name
|
|
390
|
-
|
|
390
|
+
unless remoteName.nil?
|
|
391
|
+
@parse_class = remoteName.to_s
|
|
392
|
+
Parse::Model.model_registry_changed!
|
|
393
|
+
end
|
|
391
394
|
@parse_class
|
|
392
395
|
end
|
|
393
396
|
|
|
397
|
+
# @!visibility private
|
|
398
|
+
# A new model can answer a class name {Parse::Model.find_class} already
|
|
399
|
+
# recorded as a miss, so the registry caches are reset.
|
|
400
|
+
def inherited(subclass)
|
|
401
|
+
super
|
|
402
|
+
Parse::Model.model_registry_changed!
|
|
403
|
+
end
|
|
404
|
+
|
|
394
405
|
# The set of default ACLs to be applied on newly created instances of this class.
|
|
395
406
|
# The result follows the class's {acl_policy_setting}: the shipped default
|
|
396
407
|
# policy is `:owner_else_private`, whose fallback half is {Parse::ACL.private}
|
|
@@ -1352,6 +1363,13 @@ module Parse
|
|
|
1352
1363
|
# the trust signal here.
|
|
1353
1364
|
trusted = @_trusted_init == true
|
|
1354
1365
|
@_trusted_init = nil
|
|
1366
|
+
# Accept hash-like input that is not a Hash: Rails strong parameters
|
|
1367
|
+
# (`ActionController::Parameters`) and Struct / OpenStruct values.
|
|
1368
|
+
# Strong parameters raise from `to_h` when unpermitted, so
|
|
1369
|
+
# strong-parameter filtering still applies. Other objects that merely
|
|
1370
|
+
# respond to `to_h` (Set, Range, Array) are ignored as before rather
|
|
1371
|
+
# than converted.
|
|
1372
|
+
opts = opts.to_h if self.class.hash_like_init_input?(opts)
|
|
1355
1373
|
input_hash = opts.is_a?(Hash) ? opts : nil
|
|
1356
1374
|
input_had_acl = input_hash && %w[ACL acl].any? do |key|
|
|
1357
1375
|
input_hash.key?(key) || input_hash.key?(key.to_sym)
|
|
@@ -1362,13 +1380,24 @@ module Parse
|
|
|
1362
1380
|
if opts.is_a?(String) #then it's the objectId
|
|
1363
1381
|
@id = opts.to_s
|
|
1364
1382
|
elsif opts.is_a?(Hash)
|
|
1365
|
-
# Pop the
|
|
1366
|
-
#
|
|
1367
|
-
#
|
|
1368
|
-
|
|
1383
|
+
# Pop the `as:` option before applying attributes so it is not
|
|
1384
|
+
# mistaken for a model property. It holds the caller-supplied owner
|
|
1385
|
+
# user for save-time ACL resolution, so only a Symbol key written
|
|
1386
|
+
# in code is honored. A String "as" key (form or JSON params, or any
|
|
1387
|
+
# key of a HashWithIndifferentAccess) is removed and ignored: a
|
|
1388
|
+
# request body must not be able to pick the ACL owner, e.g.
|
|
1389
|
+
# `{"as" => "*"}` to make the record public.
|
|
1390
|
+
if opts.key?(:as) || opts.key?("as")
|
|
1391
|
+
symbol_key = !opts.is_a?(ActiveSupport::HashWithIndifferentAccess) && opts.key?(:as)
|
|
1392
|
+
acl_owner_override = symbol_key ? opts[:as] : nil
|
|
1393
|
+
opts = opts.dup
|
|
1394
|
+
opts.delete(:as)
|
|
1395
|
+
opts.delete("as")
|
|
1396
|
+
self.class.validate_acl_owner_option!(acl_owner_override) unless acl_owner_override.nil?
|
|
1397
|
+
end
|
|
1369
1398
|
#if the objectId is provided we will consider the object pristine
|
|
1370
1399
|
#and not track dirty items
|
|
1371
|
-
dirty_track = opts[Parse::Model::OBJECT_ID] || opts[:objectId] || opts[:id]
|
|
1400
|
+
dirty_track = opts[Parse::Model::OBJECT_ID] || opts[:objectId] || opts[:id] || opts[Parse::Model::ID]
|
|
1372
1401
|
# Always filter the narrow PROTECTED_INITIALIZE_KEYS set unless
|
|
1373
1402
|
# the caller is a trusted hydration path. Decoupled from
|
|
1374
1403
|
# dirty_track so an objectId-bearing hash from a controller,
|
|
@@ -1398,11 +1427,18 @@ module Parse
|
|
|
1398
1427
|
# self-write-plus-public-read ACL on signup; stamping any value from
|
|
1399
1428
|
# the SDK side (even `{}`) overrides that and locks the new user out
|
|
1400
1429
|
# of editing their own profile without the master key.
|
|
1430
|
+
#
|
|
1431
|
+
# A trusted hydration of an existing row (server JSON with an objectId)
|
|
1432
|
+
# is also exempt. A row without an `ACL` key is public on the server,
|
|
1433
|
+
# so stamping the local default would misreport it (usually as
|
|
1434
|
+
# private). Such a row is never ACL-pristine either: the save-time
|
|
1435
|
+
# resolver must not replace the ACL of a record that already exists.
|
|
1436
|
+
server_row = trusted && input_had_id
|
|
1401
1437
|
acl_was_user_supplied = !self.acl.nil?
|
|
1402
|
-
unless self.class.builtin_acl_default_active?
|
|
1438
|
+
unless self.class.builtin_acl_default_active? || server_row
|
|
1403
1439
|
self.acl = self.class.default_acls.as_json if self.acl.nil?
|
|
1404
1440
|
end
|
|
1405
|
-
@_acl_pristine = !acl_was_user_supplied
|
|
1441
|
+
@_acl_pristine = !acl_was_user_supplied && !server_row
|
|
1406
1442
|
@_acl_owner_override = acl_owner_override
|
|
1407
1443
|
|
|
1408
1444
|
# Record where our ACL knowledge came from. `acl.nil?` alone is not
|
|
@@ -1467,11 +1503,89 @@ module Parse
|
|
|
1467
1503
|
Parse::Pointer.new self.parse_class, id
|
|
1468
1504
|
end
|
|
1469
1505
|
|
|
1470
|
-
#
|
|
1471
|
-
#
|
|
1472
|
-
#
|
|
1506
|
+
# Whether this object exists on the server, in the ActiveModel sense
|
|
1507
|
+
# (`form_with` uses it to choose between create and update). An object
|
|
1508
|
+
# with an objectId is persisted even when it has unsaved changes or was
|
|
1509
|
+
# built without timestamps (a pointer or an id-only instance). Use
|
|
1510
|
+
# {#changed?} to ask whether there are unsaved changes.
|
|
1511
|
+
#
|
|
1512
|
+
# The one exception is an objectId assigned client-side during a create
|
|
1513
|
+
# (`parse_reference precompute: true`, `acl_owner :self`): the object
|
|
1514
|
+
# is not persisted until the create succeeds and returns `createdAt`.
|
|
1515
|
+
#
|
|
1516
|
+
# A destroyed object is not persisted, matching ActiveModel's
|
|
1517
|
+
# `persisted? == !new_record? && !destroyed?` contract.
|
|
1518
|
+
# @return [Boolean] true if this object exists on the server.
|
|
1473
1519
|
def persisted?
|
|
1474
|
-
|
|
1520
|
+
return false if @id.blank?
|
|
1521
|
+
return false if destroyed?
|
|
1522
|
+
return @created_at.present? if defined?(@_creating_record) && @_creating_record
|
|
1523
|
+
true
|
|
1524
|
+
end
|
|
1525
|
+
|
|
1526
|
+
# Whether this object was deleted from the server by a successful
|
|
1527
|
+
# {#destroy}. A destroyed object keeps its objectId (so `dom_id`,
|
|
1528
|
+
# `to_key` and Turbo Stream removals keep working) but is no longer
|
|
1529
|
+
# {#persisted?}, and {#save} refuses to write it.
|
|
1530
|
+
# @return [Boolean]
|
|
1531
|
+
def destroyed?
|
|
1532
|
+
@_destroyed == true
|
|
1533
|
+
end
|
|
1534
|
+
|
|
1535
|
+
# An ActiveRecord-compatible cache key: `"<collection>/<objectId>"` for an
|
|
1536
|
+
# object with an id, `"<collection>/new"` otherwise. The collection
|
|
1537
|
+
# segment comes from `model_name.cache_key` (for example `"songs"` or
|
|
1538
|
+
# `"music/albums"`), so records of different classes that share an
|
|
1539
|
+
# objectId never collide in a fragment cache.
|
|
1540
|
+
# @return [String]
|
|
1541
|
+
def cache_key
|
|
1542
|
+
prefix = self.class.model_name.cache_key
|
|
1543
|
+
@id.present? ? "#{prefix}/#{@id}" : "#{prefix}/new"
|
|
1544
|
+
end
|
|
1545
|
+
|
|
1546
|
+
# The cache version, derived from `updated_at` in the same
|
|
1547
|
+
# `"%Y%m%d%H%M%S%6N"` form ActiveRecord uses. Returns nil when the
|
|
1548
|
+
# object has no `updated_at` (new or pointer-state objects). Reads the
|
|
1549
|
+
# stored value directly, so it never triggers an autofetch or changes
|
|
1550
|
+
# dirty state.
|
|
1551
|
+
# @return [String, nil]
|
|
1552
|
+
def cache_version
|
|
1553
|
+
value = @updated_at
|
|
1554
|
+
value = Parse::Date.parse(value) if value.is_a?(String)
|
|
1555
|
+
return nil unless value.respond_to?(:to_time)
|
|
1556
|
+
value.to_time.utc.strftime("%Y%m%d%H%M%S%6N")
|
|
1557
|
+
rescue ArgumentError
|
|
1558
|
+
nil
|
|
1559
|
+
end
|
|
1560
|
+
|
|
1561
|
+
# The cache key with the {#cache_version} appended, as in ActiveRecord.
|
|
1562
|
+
# @return [String]
|
|
1563
|
+
def cache_key_with_version
|
|
1564
|
+
version = cache_version
|
|
1565
|
+
version ? "#{cache_key}-#{version}" : cache_key
|
|
1566
|
+
end
|
|
1567
|
+
|
|
1568
|
+
# The current values of this object's properties, keyed by local
|
|
1569
|
+
# attribute name (as Strings).
|
|
1570
|
+
#
|
|
1571
|
+
# {#attributes} returns the property TYPE map (`{"title" => :string}`
|
|
1572
|
+
# style, with Symbol keys). That shape is load-bearing: ActiveModel
|
|
1573
|
+
# serialization reads its keys and then calls each reader, and
|
|
1574
|
+
# {#as_json} depends on it. This method is the value form. It reads the
|
|
1575
|
+
# stored values directly, so it never autofetches a pointer, never
|
|
1576
|
+
# applies defaults, and never changes dirty state; an unfetched field
|
|
1577
|
+
# reads as nil.
|
|
1578
|
+
# @return [Hash{String => Object}]
|
|
1579
|
+
def attribute_values
|
|
1580
|
+
self.class.fields.each_key.with_object({}) do |key, h|
|
|
1581
|
+
ivar = :"@#{key}"
|
|
1582
|
+
next unless self.class.field_map.key?(key)
|
|
1583
|
+
value = instance_variable_defined?(ivar) ? instance_variable_get(ivar) : nil
|
|
1584
|
+
if value.is_a?(String) && self.class.fields[key] == :date
|
|
1585
|
+
value = (Parse::Date.parse(value) rescue value)
|
|
1586
|
+
end
|
|
1587
|
+
h[key.to_s] = value
|
|
1588
|
+
end
|
|
1475
1589
|
end
|
|
1476
1590
|
|
|
1477
1591
|
# Force reload from the database and replace any local fields with data from
|
|
@@ -1618,7 +1732,9 @@ module Parse
|
|
|
1618
1732
|
@_fetched_keys = nil
|
|
1619
1733
|
else
|
|
1620
1734
|
# Always include :id and convert to symbols
|
|
1621
|
-
@_fetched_keys =
|
|
1735
|
+
@_fetched_keys = Parse::Query.with_field_aliases(parse_class) do
|
|
1736
|
+
keys.map { |k| Parse::Query.format_field(k).to_sym }
|
|
1737
|
+
end
|
|
1622
1738
|
@_fetched_keys << :id unless @_fetched_keys.include?(:id)
|
|
1623
1739
|
@_fetched_keys << :objectId unless @_fetched_keys.include?(:objectId)
|
|
1624
1740
|
@_fetched_keys.uniq!
|
|
@@ -1641,7 +1757,15 @@ module Parse
|
|
|
1641
1757
|
key = key.to_sym
|
|
1642
1758
|
# Base keys are always considered fetched
|
|
1643
1759
|
return true if Parse::Properties::BASE_KEYS.include?(key)
|
|
1644
|
-
|
|
1760
|
+
# The ACL counts as fetched only when the selective response carried
|
|
1761
|
+
# it. Treating it as always present made `acl` read nil on a partial
|
|
1762
|
+
# fetch, so `(obj.acl || Parse::ACL.new).apply(...)` replaced the
|
|
1763
|
+
# record's real ACL with a single grant on save. Reading it now
|
|
1764
|
+
# autofetches like any other field the partial fetch left out.
|
|
1765
|
+
if key == :acl || key == :ACL
|
|
1766
|
+
return @_fetched_keys.include?(:ACL) || @_fetched_keys.include?(:acl) ||
|
|
1767
|
+
instance_variable_get(:@_authorization_acl_state) != :unknown
|
|
1768
|
+
end
|
|
1645
1769
|
|
|
1646
1770
|
# Check both local key and remote field name
|
|
1647
1771
|
# Convert remote_key to symbol for consistent comparison
|
|
@@ -1704,8 +1828,25 @@ module Parse
|
|
|
1704
1828
|
@_nested_fetched_keys[field_name]
|
|
1705
1829
|
end
|
|
1706
1830
|
|
|
1831
|
+
# @!visibility private
|
|
1832
|
+
# Partial-fetch tracking after a successful save. A create leaves a fully
|
|
1833
|
+
# known object. An update returns only `updatedAt`, so fields the partial
|
|
1834
|
+
# fetch never loaded (the ACL among them) are still unknown: keep the
|
|
1835
|
+
# tracking, and count the fields just written as known. Clearing it made
|
|
1836
|
+
# an unloaded `acl` read nil, and a grant added to that nil replaced the
|
|
1837
|
+
# record's existing grants on the next save.
|
|
1838
|
+
# @param was_new [Boolean] whether the save created the object.
|
|
1839
|
+
# @param saved_fields [Array<Symbol>] the fields the save wrote.
|
|
1840
|
+
# @return [void]
|
|
1841
|
+
def _after_save_partial_fetch_state!(was_new, saved_fields)
|
|
1842
|
+
if was_new || !has_selective_keys?
|
|
1843
|
+
clear_partial_fetch_state!
|
|
1844
|
+
return
|
|
1845
|
+
end
|
|
1846
|
+
@_fetched_keys = (@_fetched_keys + Array(saved_fields).map(&:to_sym)).uniq
|
|
1847
|
+
end
|
|
1848
|
+
|
|
1707
1849
|
# Clears all partial fetch tracking state.
|
|
1708
|
-
# Called after successful save since server returns updated object.
|
|
1709
1850
|
# @return [void]
|
|
1710
1851
|
def clear_partial_fetch_state!
|
|
1711
1852
|
@_fetched_keys = nil
|
|
@@ -1777,9 +1918,73 @@ module Parse
|
|
|
1777
1918
|
# @note This does not reload the object from the persistent store, for this use "reload!" instead.
|
|
1778
1919
|
# @see #reload!
|
|
1779
1920
|
def rollback!
|
|
1921
|
+
snapshot = defined?(@_acl_snapshot_before_change) ? @_acl_snapshot_before_change : nil
|
|
1780
1922
|
restore_attributes
|
|
1923
|
+
# ACL edits are usually made in place (`acl.apply`, `acl.delete`), so
|
|
1924
|
+
# the value ActiveModel restores may be the edited object itself.
|
|
1925
|
+
# Restore from the pre-change snapshot instead, and drop the snapshot
|
|
1926
|
+
# so a later edit captures a fresh baseline.
|
|
1927
|
+
@acl = Parse::ACL.typecast(snapshot.as_json, self) if snapshot && !@acl.nil?
|
|
1928
|
+
@_acl_snapshot_before_change = nil
|
|
1929
|
+
end
|
|
1930
|
+
|
|
1931
|
+
# Keys that mass assignment never applies to an object: the objectId
|
|
1932
|
+
# under its local and remote names. An existing object's id must not be
|
|
1933
|
+
# retargeted by a params hash (`{"id" => "victim"}` would redirect the
|
|
1934
|
+
# next save to another record). A new object gets an id only through
|
|
1935
|
+
# the constructor, {Parse::Object.build}, or an explicit `id=` call.
|
|
1936
|
+
PROTECTED_IDENTITY_KEYS = %w[id objectId].freeze
|
|
1937
|
+
|
|
1938
|
+
# Mass-assign attributes with dirty tracking (the Rails form path).
|
|
1939
|
+
# Filters {Parse::Properties::PROTECTED_MASS_ASSIGNMENT_KEYS} (session
|
|
1940
|
+
# token, roles, ACL row permissions, auth data, timestamps, className)
|
|
1941
|
+
# and {PROTECTED_IDENTITY_KEYS}. Accepts a Hash or hash-like input such as
|
|
1942
|
+
# permitted `ActionController::Parameters` (unpermitted parameters raise
|
|
1943
|
+
# from `to_h`).
|
|
1944
|
+
# @param hash [Hash]
|
|
1945
|
+
def attributes=(hash)
|
|
1946
|
+
hash = hash.to_h if self.class.hash_like_init_input?(hash)
|
|
1947
|
+
return unless hash.is_a?(Hash)
|
|
1948
|
+
super(_without_identity_keys(hash))
|
|
1949
|
+
end
|
|
1950
|
+
|
|
1951
|
+
# @!visibility private
|
|
1952
|
+
# Strips {PROTECTED_IDENTITY_KEYS} on every mass-assignment
|
|
1953
|
+
# (`dirty_track: true`) call, and on hydration once the object already
|
|
1954
|
+
# has an id. Hydration of an object without an id (the constructor,
|
|
1955
|
+
# {Parse::Object.build}) may still set it.
|
|
1956
|
+
def apply_attributes!(hash, dirty_track: false, filter_protected: nil, protected_set: nil)
|
|
1957
|
+
if hash.is_a?(Hash) && (dirty_track || @id.present?)
|
|
1958
|
+
hash = _without_identity_keys(hash)
|
|
1959
|
+
end
|
|
1960
|
+
super(hash, dirty_track: dirty_track, filter_protected: filter_protected, protected_set: protected_set)
|
|
1961
|
+
end
|
|
1962
|
+
|
|
1963
|
+
private
|
|
1964
|
+
|
|
1965
|
+
# ActiveModel's `assign_attributes` funnels through this method after
|
|
1966
|
+
# its strong-parameters check. Apply the same protected-key filter as
|
|
1967
|
+
# {#attributes=} so `assign_attributes("session_token" => …, "id" => …,
|
|
1968
|
+
# "created_at" => …)` cannot set those fields.
|
|
1969
|
+
def _assign_attributes(attributes)
|
|
1970
|
+
if attributes.key?("ACL") || attributes.key?("acl") || attributes.key?(:ACL) || attributes.key?(:acl)
|
|
1971
|
+
Parse::Properties.warn_acl_mass_assignment_once!
|
|
1972
|
+
end
|
|
1973
|
+
blocked = Parse::Properties::PROTECTED_MASS_ASSIGNMENT_KEYS
|
|
1974
|
+
filtered = attributes.reject do |key, _|
|
|
1975
|
+
name = key.to_s
|
|
1976
|
+
blocked.include?(name) || PROTECTED_IDENTITY_KEYS.include?(name)
|
|
1977
|
+
end
|
|
1978
|
+
super(filtered)
|
|
1979
|
+
end
|
|
1980
|
+
|
|
1981
|
+
def _without_identity_keys(hash)
|
|
1982
|
+
return hash unless hash.any? { |key, _| PROTECTED_IDENTITY_KEYS.include?(key.to_s) }
|
|
1983
|
+
hash.reject { |key, _| PROTECTED_IDENTITY_KEYS.include?(key.to_s) }
|
|
1781
1984
|
end
|
|
1782
1985
|
|
|
1986
|
+
public
|
|
1987
|
+
|
|
1783
1988
|
# Overrides ActiveModel::Validations#validate! instance method.
|
|
1784
1989
|
# It runs all validations for this object. If validation fails,
|
|
1785
1990
|
# it raises ActiveModel::ValidationError otherwise it returns the object.
|
|
@@ -1877,7 +2082,9 @@ module Parse
|
|
|
1877
2082
|
o.instance_variable_set(:@_nested_fetched_keys, nested_fetched_keys) if nested_fetched_keys.present?
|
|
1878
2083
|
if fetched_keys.present?
|
|
1879
2084
|
# Process fetched_keys like the setter does - convert to symbols and include :id
|
|
1880
|
-
processed_keys =
|
|
2085
|
+
processed_keys = Parse::Query.with_field_aliases(klass.parse_class) do
|
|
2086
|
+
fetched_keys.map { |k| Parse::Query.format_field(k).to_sym }
|
|
2087
|
+
end
|
|
1881
2088
|
processed_keys << :id unless processed_keys.include?(:id)
|
|
1882
2089
|
processed_keys << :objectId unless processed_keys.include?(:objectId)
|
|
1883
2090
|
processed_keys.uniq!
|
|
@@ -2025,10 +2232,51 @@ module Parse
|
|
|
2025
2232
|
return owner.id if owner.id.present?
|
|
2026
2233
|
return nil
|
|
2027
2234
|
end
|
|
2028
|
-
return owner if
|
|
2235
|
+
return owner if self.class.acl_owner_id_string?(owner)
|
|
2029
2236
|
nil
|
|
2030
2237
|
end
|
|
2031
2238
|
|
|
2239
|
+
class << self
|
|
2240
|
+
# @!visibility private
|
|
2241
|
+
# Whether `opts` is hash-like input that {#initialize} converts with
|
|
2242
|
+
# `to_h`: Rails strong parameters (`permitted?` / `to_unsafe_h`) or a
|
|
2243
|
+
# Struct / OpenStruct. Hash, String, Array and nil are handled by
|
|
2244
|
+
# {#initialize} directly; other objects are not converted.
|
|
2245
|
+
def hash_like_init_input?(opts)
|
|
2246
|
+
return false if opts.nil? || opts.is_a?(Hash) || opts.is_a?(String) || opts.is_a?(Array)
|
|
2247
|
+
return false unless opts.respond_to?(:to_h)
|
|
2248
|
+
return true if opts.respond_to?(:permitted?) || opts.respond_to?(:to_unsafe_h)
|
|
2249
|
+
return true if opts.is_a?(Struct)
|
|
2250
|
+
defined?(::OpenStruct) && opts.is_a?(::OpenStruct) ? true : false
|
|
2251
|
+
end
|
|
2252
|
+
|
|
2253
|
+
# @!visibility private
|
|
2254
|
+
# Whether `value` is usable as a raw user objectId for an ACL owner.
|
|
2255
|
+
# Refuses the public key "*" and role keys ("role:Name"), which would
|
|
2256
|
+
# grant the record to everyone or to a role instead of to one user.
|
|
2257
|
+
def acl_owner_id_string?(value)
|
|
2258
|
+
value.is_a?(String) && value.present? && value != "*" && !value.start_with?("role:")
|
|
2259
|
+
end
|
|
2260
|
+
|
|
2261
|
+
# @!visibility private
|
|
2262
|
+
# Validates the `as:` owner option given to {#initialize}. Accepts a
|
|
2263
|
+
# Parse::User, a Parse::Pointer to `_User` with an objectId, or a raw
|
|
2264
|
+
# user objectId String.
|
|
2265
|
+
# @raise [ArgumentError] for any other value.
|
|
2266
|
+
def validate_acl_owner_option!(owner)
|
|
2267
|
+
if owner.is_a?(Parse::Pointer)
|
|
2268
|
+
return if owner.parse_class == Parse::Model::CLASS_USER && owner.id.present?
|
|
2269
|
+
raise ArgumentError,
|
|
2270
|
+
"as: must be a Parse::User or a pointer to _User with an objectId " \
|
|
2271
|
+
"(got a #{owner.parse_class} #{owner.class})."
|
|
2272
|
+
end
|
|
2273
|
+
return if acl_owner_id_string?(owner)
|
|
2274
|
+
raise ArgumentError,
|
|
2275
|
+
"as: must be a Parse::User, a pointer to _User, or a user objectId String; " \
|
|
2276
|
+
"the public key \"*\" and role keys are not accepted."
|
|
2277
|
+
end
|
|
2278
|
+
end
|
|
2279
|
+
|
|
2032
2280
|
set_callback :save, :before, :_resolve_default_acl
|
|
2033
2281
|
|
|
2034
2282
|
# Override acl_will_change! to capture a snapshot of the ACL before modification.
|
data/lib/parse/model/phone.rb
CHANGED
|
@@ -71,7 +71,25 @@ module Parse
|
|
|
71
71
|
# Regex to strip non-digit characters (except +)
|
|
72
72
|
STRIP_NON_DIGITS = /[^\d+]/
|
|
73
73
|
|
|
74
|
+
# A 10-digit North American Numbering Plan number: area code and
|
|
75
|
+
# exchange code each start with 2-9.
|
|
76
|
+
NANP_NATIONAL = /\A[2-9]\d{2}[2-9]\d{6}\z/
|
|
77
|
+
|
|
74
78
|
class << self
|
|
79
|
+
# Country calling code (digits only, no `+`) assumed for numbers
|
|
80
|
+
# entered without a leading `+`. Defaults to `"1"` (North America),
|
|
81
|
+
# under which 10-digit numbers get `+1`. Set to another code (for
|
|
82
|
+
# example `"44"`) to apply that country to national numbers, or to
|
|
83
|
+
# `nil` to make no assumption, in which case a national number
|
|
84
|
+
# without `+` is left unprefixed and fails validation.
|
|
85
|
+
# @return [String, nil]
|
|
86
|
+
attr_writer :default_country_code
|
|
87
|
+
|
|
88
|
+
# @return [String, nil] see {default_country_code=}.
|
|
89
|
+
def default_country_code
|
|
90
|
+
defined?(@default_country_code) ? @default_country_code : "1"
|
|
91
|
+
end
|
|
92
|
+
|
|
75
93
|
# Check if phonelib is available for enhanced validation
|
|
76
94
|
# @return [Boolean] true if phonelib gem is loaded
|
|
77
95
|
def phonelib_available?
|
|
@@ -131,7 +149,25 @@ module Parse
|
|
|
131
149
|
end
|
|
132
150
|
|
|
133
151
|
# Normalize a phone number string to E.164 format.
|
|
134
|
-
# Removes all non-digit characters except leading +.
|
|
152
|
+
# Removes all non-digit characters except a leading +.
|
|
153
|
+
#
|
|
154
|
+
# Input that already starts with `+` is kept as dialed. Input without
|
|
155
|
+
# a `+` is resolved as follows, identically with and without phonelib
|
|
156
|
+
# (phonelib parses the normalized result):
|
|
157
|
+
#
|
|
158
|
+
# 1. A leading `00` (international access prefix) becomes `+`.
|
|
159
|
+
# 2. With {Parse::Phone.default_country_code} set to `"1"` (the
|
|
160
|
+
# default), a 10-digit North American number (`4155551234`,
|
|
161
|
+
# `(415) 555-1234`) becomes `+14155551234`, and an 11-digit number
|
|
162
|
+
# starting with `1` gets a `+`.
|
|
163
|
+
# 3. Any other number of 11 or more digits that does not start with
|
|
164
|
+
# `0` is assumed to already include its country code.
|
|
165
|
+
# 4. A shorter national number (or one with a leading trunk `0`) gets
|
|
166
|
+
# the default country code, minus the trunk `0`, when that code is
|
|
167
|
+
# not `"1"`. With no default country code it is left without a `+`
|
|
168
|
+
# so it fails validation instead of being read as some other
|
|
169
|
+
# country's code (previously `4155551234` became the Swiss
|
|
170
|
+
# `+41 55 551 234`).
|
|
135
171
|
#
|
|
136
172
|
# @param value [String] the phone number string
|
|
137
173
|
# @return [String, nil] the normalized number or nil if invalid
|
|
@@ -140,12 +176,25 @@ module Parse
|
|
|
140
176
|
|
|
141
177
|
# Remove all non-digit characters except +
|
|
142
178
|
cleaned = value.to_s.gsub(STRIP_NON_DIGITS, "")
|
|
179
|
+
return cleaned if cleaned.start_with?("+")
|
|
143
180
|
|
|
144
|
-
|
|
145
|
-
|
|
181
|
+
digits = cleaned.delete("+")
|
|
182
|
+
return "+#{digits}" if digits.empty?
|
|
183
|
+
return "+#{digits[2..]}" if digits.start_with?("00")
|
|
146
184
|
|
|
147
|
-
|
|
148
|
-
|
|
185
|
+
default_cc = Parse::Phone.default_country_code.to_s.delete("+").presence
|
|
186
|
+
if default_cc == "1"
|
|
187
|
+
return "+1#{digits}" if NANP_NATIONAL.match?(digits)
|
|
188
|
+
return "+#{digits}" if digits.length == 11 && digits.start_with?("1") && NANP_NATIONAL.match?(digits[1..])
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
return "+#{digits}" if digits.length >= 11 && !digits.start_with?("0")
|
|
192
|
+
|
|
193
|
+
if default_cc && default_cc != "1"
|
|
194
|
+
"+#{default_cc}#{digits.sub(/\A0/, "")}"
|
|
195
|
+
else
|
|
196
|
+
digits
|
|
197
|
+
end
|
|
149
198
|
end
|
|
150
199
|
|
|
151
200
|
# @return [String, nil] the E.164 formatted phone number
|
data/lib/parse/model/pointer.rb
CHANGED
|
@@ -216,7 +216,9 @@ module Parse
|
|
|
216
216
|
# Build query parameters for partial fetch
|
|
217
217
|
query = {}
|
|
218
218
|
if keys.present?
|
|
219
|
-
keys_array =
|
|
219
|
+
keys_array = Parse::Query.with_field_aliases(parse_class) do
|
|
220
|
+
Array(keys).map { |k| Parse::Query.format_field(k) }
|
|
221
|
+
end
|
|
220
222
|
query[:keys] = keys_array.join(",")
|
|
221
223
|
end
|
|
222
224
|
if includes.present?
|
|
@@ -246,7 +248,9 @@ module Parse
|
|
|
246
248
|
# For partial fetch, build with fetched_keys tracking
|
|
247
249
|
if keys.present?
|
|
248
250
|
# Parse keys to get top-level field names and nested keys
|
|
249
|
-
top_level_keys =
|
|
251
|
+
top_level_keys = Parse::Query.with_field_aliases(parse_class) do
|
|
252
|
+
Array(keys).map { |k| Parse::Query.format_field(k).split(".").first.to_sym }
|
|
253
|
+
end
|
|
250
254
|
top_level_keys << :id unless top_level_keys.include?(:id)
|
|
251
255
|
top_level_keys << :objectId unless top_level_keys.include?(:objectId)
|
|
252
256
|
top_level_keys.uniq!
|
|
@@ -278,7 +282,9 @@ module Parse
|
|
|
278
282
|
def fetch_json(keys: nil, includes: nil)
|
|
279
283
|
query = {}
|
|
280
284
|
if keys.present?
|
|
281
|
-
keys_array =
|
|
285
|
+
keys_array = Parse::Query.with_field_aliases(parse_class) do
|
|
286
|
+
Array(keys).map { |k| Parse::Query.format_field(k) }
|
|
287
|
+
end
|
|
282
288
|
query[:keys] = keys_array.join(",")
|
|
283
289
|
end
|
|
284
290
|
if includes.present?
|
|
@@ -314,10 +320,15 @@ module Parse
|
|
|
314
320
|
end
|
|
315
321
|
|
|
316
322
|
# Two Parse::Pointers (or Parse::Objects) are equal if both of them have
|
|
317
|
-
# the same Parse class and the same id.
|
|
323
|
+
# the same Parse class and the same id. An instance without an id (an
|
|
324
|
+
# unsaved object) is equal only to itself, as in ActiveRecord: two
|
|
325
|
+
# distinct new records are different records even though neither has
|
|
326
|
+
# an id yet.
|
|
318
327
|
# @return [Boolean]
|
|
319
328
|
def ==(o)
|
|
329
|
+
return true if equal?(o)
|
|
320
330
|
return false unless o.is_a?(Pointer)
|
|
331
|
+
return false if id.blank? || o.id.blank?
|
|
321
332
|
#only equal if the Parse class and object ID are the same.
|
|
322
333
|
self.parse_class == o.parse_class && id == o.id
|
|
323
334
|
end
|
|
@@ -333,8 +344,14 @@ module Parse
|
|
|
333
344
|
# - Hash key lookups to find objects by identity
|
|
334
345
|
# - Set operations
|
|
335
346
|
#
|
|
347
|
+
# An instance without an id hashes by Ruby identity, consistent with
|
|
348
|
+
# {#==}. Its hash therefore changes once it is saved and receives an id,
|
|
349
|
+
# so re-index any Hash or Set that holds it as a key after saving (the
|
|
350
|
+
# same caveat as ActiveRecord).
|
|
351
|
+
#
|
|
336
352
|
# @return [Integer] hash code based on class name and object id
|
|
337
353
|
def hash
|
|
354
|
+
return super if id.blank?
|
|
338
355
|
[parse_class, id].hash
|
|
339
356
|
end
|
|
340
357
|
|
|
@@ -370,6 +387,12 @@ module Parse
|
|
|
370
387
|
# @return [Object] the result of calling the method on the fetched object
|
|
371
388
|
# @raise [Parse::AutofetchTriggeredError] if autofetch_raise_on_missing_keys is enabled
|
|
372
389
|
def method_missing(method_name, *args, &block)
|
|
390
|
+
# A Parse::Object defines real accessors for its fields, so reaching
|
|
391
|
+
# here means a name with no accessor (for example the remote alias
|
|
392
|
+
# `objectId=` sent by ActiveModel's `assign_attributes`). Autofetching
|
|
393
|
+
# would issue a network request and run the call on a separate fetched
|
|
394
|
+
# copy, not on this object, so raise NoMethodError instead.
|
|
395
|
+
return super if is_a?(Parse::Object)
|
|
373
396
|
# Try to find the model class for this pointer
|
|
374
397
|
klass = Parse::Model.find_class(parse_class)
|
|
375
398
|
|
|
@@ -381,6 +404,16 @@ module Parse
|
|
|
381
404
|
# We have a registered class with this field - handle autofetch
|
|
382
405
|
field_name = method_name.to_s.chomp("=").to_sym
|
|
383
406
|
|
|
407
|
+
# A setter would change a fetched copy held inside this pointer, which
|
|
408
|
+
# nothing ever saves, so the write would be lost without notice.
|
|
409
|
+
if method_name.to_s.end_with?("=")
|
|
410
|
+
raise NoMethodError.new(
|
|
411
|
+
"undefined method '#{method_name}' for a #{self.class} to #{parse_class}. A pointer " \
|
|
412
|
+
"cannot be modified: fetch the object first (pointer.fetch) and set :#{field_name} on it.",
|
|
413
|
+
method_name,
|
|
414
|
+
)
|
|
415
|
+
end
|
|
416
|
+
|
|
384
417
|
# If autofetch_raise_on_missing_keys is enabled, raise an error
|
|
385
418
|
if Parse.autofetch_raise_on_missing_keys
|
|
386
419
|
raise Parse::AutofetchTriggeredError.new(klass, id, field_name, is_pointer: true)
|
|
@@ -405,9 +438,10 @@ module Parse
|
|
|
405
438
|
# @param include_private [Boolean] whether to include private methods
|
|
406
439
|
# @return [Boolean] true if the method can be handled
|
|
407
440
|
def respond_to_missing?(method_name, include_private = false)
|
|
441
|
+
return super if is_a?(Parse::Object)
|
|
408
442
|
klass = Parse::Model.find_class(parse_class)
|
|
409
|
-
if klass && klass.respond_to?(:fields)
|
|
410
|
-
field_name = method_name.
|
|
443
|
+
if klass && klass.respond_to?(:fields) && !method_name.to_s.end_with?("=")
|
|
444
|
+
field_name = method_name.to_sym
|
|
411
445
|
return true if klass.fields[field_name]
|
|
412
446
|
end
|
|
413
447
|
super
|