parse-stack-next 5.2.1 → 5.3.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/.bundle/config +1 -0
- data/CHANGELOG.md +155 -0
- data/Gemfile.lock +1 -1
- data/README.md +136 -0
- data/Rakefile +193 -40
- data/docs/client_sdk_guide.md +33 -0
- data/docs/mcp_guide.md +60 -0
- data/lib/parse/client.rb +119 -7
- data/lib/parse/model/associations/belongs_to.rb +47 -0
- data/lib/parse/model/classes/user.rb +20 -0
- data/lib/parse/model/core/pluralized_aliases.rb +30 -0
- data/lib/parse/model/core/properties.rb +27 -0
- data/lib/parse/model/core/querying.rb +70 -0
- data/lib/parse/model/file.rb +35 -2
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +156 -1
- data/lib/parse/webhooks/payload.rb +147 -4
- metadata +2 -1
data/lib/parse/stack.rb
CHANGED
|
@@ -548,6 +548,21 @@ module Parse
|
|
|
548
548
|
# PARSE_STRICT_POINTER_SHAPES=true
|
|
549
549
|
@strict_pointer_shapes = ENV["PARSE_STRICT_POINTER_SHAPES"] == "true"
|
|
550
550
|
|
|
551
|
+
# Configuration for automatic pluralized class-name aliases. When enabled
|
|
552
|
+
# (the default), referencing the plural form of a {Parse::Object} subclass
|
|
553
|
+
# constant resolves to that class, so `Posts.where(...)` works for a class
|
|
554
|
+
# `Post`. The alias is created lazily on first reference via `const_missing`
|
|
555
|
+
# and points at the same class object, so every class method
|
|
556
|
+
# (`where`, `query`, `count`, `find`, `all`, scopes) works for free and
|
|
557
|
+
# `Posts.parse_class` still returns `"Post"`. Classes whose name already
|
|
558
|
+
# ends in `s` are skipped. Set to false to opt out globally.
|
|
559
|
+
# @example Opt out globally
|
|
560
|
+
# Parse.pluralized_aliases = false
|
|
561
|
+
# @example ENV opt-out
|
|
562
|
+
# PARSE_PLURALIZED_ALIASES=false
|
|
563
|
+
# @see Parse::Core::Querying#pluralized_alias!
|
|
564
|
+
@pluralized_aliases = ENV["PARSE_PLURALIZED_ALIASES"] != "false"
|
|
565
|
+
|
|
551
566
|
# Tuning bundle for the synchronize-create lock. Per-call kwargs override.
|
|
552
567
|
# Keys: :ttl (seconds, default 3, max 30), :wait (seconds, default 2.0,
|
|
553
568
|
# max 30), :on_degraded (:warn, :warn_throttled, :raise, :proceed).
|
|
@@ -630,7 +645,8 @@ module Parse
|
|
|
630
645
|
:rewrite_lookups, :strict_property_redefinition,
|
|
631
646
|
:synchronize_create_default, :synchronize_create_options, :synchronize_create_secret,
|
|
632
647
|
:synchronize_create_store, :synchronize_classes,
|
|
633
|
-
:strict_pointer_shapes, :suppress_server_version_warning
|
|
648
|
+
:strict_pointer_shapes, :suppress_server_version_warning,
|
|
649
|
+
:pluralized_aliases
|
|
634
650
|
|
|
635
651
|
# Check whether the Parse Server version deprecation warning is
|
|
636
652
|
# silenced. Returns true if either the in-process accessor or the
|
|
@@ -714,6 +730,140 @@ module Parse
|
|
|
714
730
|
@strict_pointer_shapes == true
|
|
715
731
|
end
|
|
716
732
|
|
|
733
|
+
# Whether automatic pluralized class-name aliases are enabled. Defaults
|
|
734
|
+
# to true; opt out with `Parse.pluralized_aliases = false` or
|
|
735
|
+
# `PARSE_PLURALIZED_ALIASES=false`. See {Parse.pluralized_aliases}.
|
|
736
|
+
# @return [Boolean]
|
|
737
|
+
def pluralized_aliases?
|
|
738
|
+
@pluralized_aliases != false
|
|
739
|
+
end
|
|
740
|
+
|
|
741
|
+
# @!visibility private
|
|
742
|
+
# Resolve a (possibly plural) missing constant to its singular
|
|
743
|
+
# {Parse::Object} subclass and install the alias on the referencing
|
|
744
|
+
# module. Returns the class when an alias was created, otherwise nil so
|
|
745
|
+
# the caller (`const_missing`) can fall through to `super` and preserve
|
|
746
|
+
# normal `NameError` / autoloading behavior.
|
|
747
|
+
#
|
|
748
|
+
# Guards (fail-through to nil unless ALL hold):
|
|
749
|
+
# - the feature is enabled,
|
|
750
|
+
# - {Parse::Object} is loaded,
|
|
751
|
+
# - the name singularizes to a *different* string (i.e. looks plural),
|
|
752
|
+
# - the singular form does NOT already end in `s` (per design: classes
|
|
753
|
+
# whose name ends in `s` are not auto-aliased),
|
|
754
|
+
# - the singular constant is defined (searching ancestors so a
|
|
755
|
+
# top-level model is visible from a nested reference) and is a
|
|
756
|
+
# `Parse::Object` subclass,
|
|
757
|
+
# - the plural is not already defined on the referencing module.
|
|
758
|
+
#
|
|
759
|
+
# @param mod [Module] the module/class on which `const_missing` fired.
|
|
760
|
+
# @param name [Symbol] the missing constant name.
|
|
761
|
+
# @return [Class, nil]
|
|
762
|
+
def __pluralized_alias_for(mod, name)
|
|
763
|
+
return nil unless pluralized_aliases?
|
|
764
|
+
return nil unless defined?(Parse::Object)
|
|
765
|
+
str = name.to_s
|
|
766
|
+
singular = str.singularize
|
|
767
|
+
return nil if singular == str
|
|
768
|
+
return nil if singular.end_with?("s")
|
|
769
|
+
sym = singular.to_sym
|
|
770
|
+
return nil unless mod.const_defined?(sym, true)
|
|
771
|
+
klass = mod.const_get(sym)
|
|
772
|
+
return nil unless klass.is_a?(Class) && klass < Parse::Object
|
|
773
|
+
return nil if mod.const_defined?(name, false)
|
|
774
|
+
mod.const_set(name, klass)
|
|
775
|
+
klass
|
|
776
|
+
rescue NameError, LoadError
|
|
777
|
+
# const_get/const_defined? can raise on malformed names or autoload
|
|
778
|
+
# failures; never let alias resolution mask the original lookup.
|
|
779
|
+
nil
|
|
780
|
+
end
|
|
781
|
+
|
|
782
|
+
# Verify that every association target across the loaded {Parse::Object}
|
|
783
|
+
# subclasses resolves to a known Parse class. Covers `belongs_to` and
|
|
784
|
+
# `property … as:` pointer targets (via each class's `references`),
|
|
785
|
+
# `has_many … through: :relation` targets (via `relations`), and the
|
|
786
|
+
# query- and array-backed `has_many` targets (via `has_many_associations`)
|
|
787
|
+
# — the bucket where an `as:` typo otherwise stays latent until the
|
|
788
|
+
# association is first traversed at call time.
|
|
789
|
+
#
|
|
790
|
+
# This is the deferred companion to the definition-time scalar guard in
|
|
791
|
+
# {Parse::Associations::BelongsTo::ClassMethods#belongs_to}: at declaration
|
|
792
|
+
# time a forward reference (a target class that is required later) is legal
|
|
793
|
+
# and indistinguishable from a typo, so the cross-class resolution check is
|
|
794
|
+
# run here — after all models are loaded. Intended to run once at boot, in
|
|
795
|
+
# CI, or from a rake task ("during the upgrade").
|
|
796
|
+
#
|
|
797
|
+
# A target resolves when it is a Parse system class (`_User`, `_Role`,
|
|
798
|
+
# `_Installation`, `_Session`, …) or a registered {Parse::Object} subclass
|
|
799
|
+
# (via {Parse::Model.find_class}). Note this checks against *loaded Ruby
|
|
800
|
+
# models*: if you intentionally point at a server-side class that has no
|
|
801
|
+
# Ruby model, define a stub model for it or exclude it via `classes:`.
|
|
802
|
+
#
|
|
803
|
+
# @param classes [Array<Class>, nil] optional subset of Parse::Object
|
|
804
|
+
# subclasses to check; defaults to every loaded subclass.
|
|
805
|
+
# @raise [ArgumentError] if any target is unresolved, listing each
|
|
806
|
+
# offending `Class#field -> 'Target'`.
|
|
807
|
+
# @return [true] when every association target resolves.
|
|
808
|
+
def validate_associations!(classes: nil)
|
|
809
|
+
models = classes || Parse::Object.descendants
|
|
810
|
+
problems = []
|
|
811
|
+
models.each do |klass|
|
|
812
|
+
next unless klass.respond_to?(:parse_class)
|
|
813
|
+
if klass.respond_to?(:references)
|
|
814
|
+
klass.references.each do |field, target|
|
|
815
|
+
next if _association_target_resolvable?(target)
|
|
816
|
+
# `references` is keyed by the remote (camelCase) column; report the
|
|
817
|
+
# declared Ruby accessor so the operator can find the offending line.
|
|
818
|
+
accessor = (klass.respond_to?(:field_map) && klass.field_map.key(field)) || field
|
|
819
|
+
problems << "#{klass}##{accessor} -> #{target.inspect} (no such Parse class)"
|
|
820
|
+
end
|
|
821
|
+
end
|
|
822
|
+
if klass.respond_to?(:relations)
|
|
823
|
+
klass.relations.each do |field, target|
|
|
824
|
+
next if _association_target_resolvable?(target)
|
|
825
|
+
problems << "#{klass}##{field} (relation) -> #{target.inspect} (no such Parse class)"
|
|
826
|
+
end
|
|
827
|
+
end
|
|
828
|
+
if klass.respond_to?(:has_many_associations)
|
|
829
|
+
klass.has_many_associations.each do |accessor, meta|
|
|
830
|
+
# `:relation`-storage has_many is mirrored into `relations` and is
|
|
831
|
+
# already reported above; only the `:query` and `:array` storage
|
|
832
|
+
# targets (which live nowhere else) need checking here. This is the
|
|
833
|
+
# branch where a `has_many … as:` typo hides, since a query-backed
|
|
834
|
+
# has_many resolves its target lazily at call time.
|
|
835
|
+
next if meta[:storage] == :relation
|
|
836
|
+
target = meta[:target_class]
|
|
837
|
+
next if target.nil? || _association_target_resolvable?(target)
|
|
838
|
+
problems << "#{klass}##{accessor} (has_many #{meta[:storage]}) -> " \
|
|
839
|
+
"#{target.inspect} (no such Parse class)"
|
|
840
|
+
end
|
|
841
|
+
end
|
|
842
|
+
end
|
|
843
|
+
unless problems.empty?
|
|
844
|
+
raise ArgumentError,
|
|
845
|
+
"Unresolved Parse association targets:\n " + problems.join("\n ") +
|
|
846
|
+
"\nRequire/define the target class, or fix the `as:`/`class_name:` name."
|
|
847
|
+
end
|
|
848
|
+
true
|
|
849
|
+
end
|
|
850
|
+
|
|
851
|
+
# @!visibility private
|
|
852
|
+
# Whether an association target class name resolves to a known Parse
|
|
853
|
+
# class. Parse system classes resolve against {Parse::Model::SYSTEM_CLASS_MAP}
|
|
854
|
+
# — both the canonical `_`-prefixed value (`_User`) and the bare-name key
|
|
855
|
+
# (`User`) — even when their Ruby class is not loaded; everything else must
|
|
856
|
+
# resolve via {Parse::Model.find_class}. A leading underscore is NOT a
|
|
857
|
+
# blanket pass: a typo'd system name such as `_Usr` is neither in the map
|
|
858
|
+
# nor a registered model, so it is still surfaced as unresolved.
|
|
859
|
+
def _association_target_resolvable?(target)
|
|
860
|
+
name = target.to_s
|
|
861
|
+
return false if name.empty?
|
|
862
|
+
return true if Parse::Model::SYSTEM_CLASS_MAP.key?(name) ||
|
|
863
|
+
Parse::Model::SYSTEM_CLASS_MAP.value?(name)
|
|
864
|
+
!Parse::Model.find_class(name).nil?
|
|
865
|
+
end
|
|
866
|
+
|
|
717
867
|
# Check if MCP server feature is enabled
|
|
718
868
|
# Requires PARSE_MCP_ENABLED=true in environment AND Parse.mcp_server_enabled = true
|
|
719
869
|
# @return [Boolean]
|
|
@@ -851,4 +1001,9 @@ end
|
|
|
851
1001
|
# the setter on load.
|
|
852
1002
|
Parse._attach_slow_query_subscriber! if Parse.slow_query_threshold_ms
|
|
853
1003
|
|
|
1004
|
+
# Install the lazy pluralized class-name alias hook (Posts -> Post). Loaded
|
|
1005
|
+
# last so Parse::Object and the Parse.__pluralized_alias_for helper are
|
|
1006
|
+
# already defined. Gated at runtime on Parse.pluralized_aliases?.
|
|
1007
|
+
require_relative "model/core/pluralized_aliases"
|
|
1008
|
+
|
|
854
1009
|
require_relative "stack/railtie" if defined?(::Rails)
|
|
@@ -64,6 +64,18 @@ module Parse
|
|
|
64
64
|
attr_accessor :master, :user, :installation_id, :params, :function_name, :object, :trigger_name
|
|
65
65
|
attr_accessor :query, :log, :objects
|
|
66
66
|
attr_accessor :original, :update, :raw
|
|
67
|
+
# @!attribute [r] session_token
|
|
68
|
+
# The caller's live Parse session token, captured from the incoming
|
|
69
|
+
# webhook payload (`user.sessionToken`) before credentials are scrubbed
|
|
70
|
+
# from {#user} / {#object} / {#original} / {#update}. Present only when
|
|
71
|
+
# the originating request was made by a logged-in user -- a master-key
|
|
72
|
+
# request carries no user and no token, so this is +nil+. It is
|
|
73
|
+
# intentionally NOT one of {ATTRIBUTES}, so it never appears in
|
|
74
|
+
# {#as_json} or in the redacted request log. Reach for it (or the
|
|
75
|
+
# higher-level {#user_client} / {#user_agent}) only when a handler
|
|
76
|
+
# deliberately wants to act on the server as the calling user.
|
|
77
|
+
# @return [String, nil]
|
|
78
|
+
attr_reader :session_token
|
|
67
79
|
# @!visibility private
|
|
68
80
|
attr_accessor :webhook_class
|
|
69
81
|
alias_method :installationId, :installation_id
|
|
@@ -78,6 +90,15 @@ module Parse
|
|
|
78
90
|
hash = Hash[hash.map { |k, v| [k.to_s.underscore.to_sym, v] }]
|
|
79
91
|
@raw = hash
|
|
80
92
|
@master = hash[:master]
|
|
93
|
+
# Capture the caller's session token from the *unscrubbed* user hash
|
|
94
|
+
# before scrub_credentials strips it below. Parse Server includes
|
|
95
|
+
# `user.sessionToken` on every trigger fired by a logged-in caller
|
|
96
|
+
# (it is absent for master-key-originated requests). Pulling it aside
|
|
97
|
+
# here -- rather than leaving it in @user -- keeps it out of any object
|
|
98
|
+
# a handler might persist and out of #as_json / the request log, while
|
|
99
|
+
# still letting a handler opt in to acting as the calling user via
|
|
100
|
+
# #session_token / #user_client / #user_agent.
|
|
101
|
+
@session_token = self.class.extract_session_token(hash[:user])
|
|
81
102
|
# Webhook trigger payloads (beforeSave/afterSave/etc.) are delivered by
|
|
82
103
|
# Parse Server and, when a webhook key is configured (the default; see
|
|
83
104
|
# Parse::Webhooks.allow_unauthenticated for the opt-out used in tests /
|
|
@@ -85,7 +106,8 @@ module Parse
|
|
|
85
106
|
# server-authoritative state. A handler is meant to receive the full
|
|
86
107
|
# object -- createdAt/updatedAt, ACL, internal fields and all. The only
|
|
87
108
|
# thing stripped here is genuine credential material a handler never
|
|
88
|
-
# legitimately needs to read (live session tokens
|
|
109
|
+
# legitimately needs to read inline (live session tokens -- captured
|
|
110
|
+
# above for opt-in user-scoped clients first -- and offline-crackable
|
|
89
111
|
# password hashes); see WEBHOOK_TRIGGER_CREDENTIAL_KEYS. Protection
|
|
90
112
|
# against *persisting* forged privileged fields lives on the write path
|
|
91
113
|
# (changes_payload emits only declared, dirty-tracked properties), not on
|
|
@@ -144,11 +166,39 @@ module Parse
|
|
|
144
166
|
end
|
|
145
167
|
end
|
|
146
168
|
|
|
169
|
+
# @!visibility private
|
|
170
|
+
# Pulls the caller's session token out of the (unscrubbed) +user+ hash.
|
|
171
|
+
# Parse Server sends it as the camelCase string key +sessionToken+; this
|
|
172
|
+
# tolerates a symbol key and the snake_case form too, mirroring the
|
|
173
|
+
# leniency in +scrub_credentials+. Returns +nil+ for a blank token or a
|
|
174
|
+
# non-Hash / absent user (a master-key request has no user).
|
|
175
|
+
def self.extract_session_token(user_hash)
|
|
176
|
+
return nil unless user_hash.is_a?(Hash)
|
|
177
|
+
token = user_hash["sessionToken"] || user_hash[:sessionToken] ||
|
|
178
|
+
user_hash["session_token"] || user_hash[:session_token]
|
|
179
|
+
token = token.to_s.strip
|
|
180
|
+
token.empty? ? nil : token
|
|
181
|
+
end
|
|
182
|
+
|
|
147
183
|
# @return [ATTRIBUTES]
|
|
148
184
|
def attributes
|
|
149
185
|
ATTRIBUTES
|
|
150
186
|
end
|
|
151
187
|
|
|
188
|
+
# Redacted inspection. The default Ruby `#inspect` would dump every ivar,
|
|
189
|
+
# including the captured `@session_token` and the *pre-scrub* `@raw` hash
|
|
190
|
+
# (which still holds the caller's sessionToken and any password hashes).
|
|
191
|
+
# That is exactly the surface an error reporter or a stray `p payload`
|
|
192
|
+
# hits, so show only non-sensitive routing fields and a boolean for the
|
|
193
|
+
# token's presence. Use #as_json / the individual accessors for the
|
|
194
|
+
# (already credential-scrubbed) object data.
|
|
195
|
+
def inspect
|
|
196
|
+
"#<#{self.class.name} trigger=#{@trigger_name.inspect} " \
|
|
197
|
+
"function=#{@function_name.inspect} class=#{parse_class.inspect} " \
|
|
198
|
+
"id=#{parse_id.inspect} master=#{@master ? true : false} " \
|
|
199
|
+
"session_token=#{@session_token ? "[FILTERED]" : "nil"}>"
|
|
200
|
+
end
|
|
201
|
+
|
|
152
202
|
# Method to print to standard that utilizes the an internal id to make it easier
|
|
153
203
|
# to trace incoming requests.
|
|
154
204
|
def wlog(s)
|
|
@@ -170,6 +220,51 @@ module Parse
|
|
|
170
220
|
@master.present?
|
|
171
221
|
end
|
|
172
222
|
|
|
223
|
+
# true if this payload carried a caller session token -- i.e. the
|
|
224
|
+
# originating request was made by a logged-in user rather than the
|
|
225
|
+
# master key, so {#user_client} / {#user_agent} can act as that user.
|
|
226
|
+
# @return [Boolean]
|
|
227
|
+
def session_token?
|
|
228
|
+
!@session_token.nil?
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# An opt-in, user-scoped {Parse::Client} for acting on the server as the
|
|
232
|
+
# webhook's calling user. It mirrors the default client's connection
|
|
233
|
+
# settings (+server_url+, +application_id+, +api_key+) but carries NO
|
|
234
|
+
# master key and BINDS the caller's {#session_token}, so every request it
|
|
235
|
+
# makes -- with no further ceremony -- is authorized by Parse Server as
|
|
236
|
+
# that user: ACL, CLP and +protectedFields+ are all enforced. (A
|
|
237
|
+
# `Parse.with_session` block still overrides the bound token if you need
|
|
238
|
+
# to act as someone else within a call.) Memoized per payload, since each
|
|
239
|
+
# webhook delivery carries a distinct token.
|
|
240
|
+
# @return [Parse::Client, nil] +nil+ when the payload carried no token.
|
|
241
|
+
def user_client
|
|
242
|
+
return nil if @session_token.nil?
|
|
243
|
+
@user_client ||= Parse::Client.client.become(@session_token)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# An opt-in, non-master {Parse::Agent} scoped to the webhook caller's
|
|
247
|
+
# session token. Because its client has no master key and it is built
|
|
248
|
+
# with a non-empty +session_token:+, the agent runs in CLIENT MODE:
|
|
249
|
+
# every tool/query routes through a path Parse Server (or the SDK's own
|
|
250
|
+
# ACL/CLP enforcement layer) authorizes as the calling user, with no
|
|
251
|
+
# master-key fallback to silently bypass row-level security. This is the
|
|
252
|
+
# handle to use when a handler should read or act strictly within the
|
|
253
|
+
# caller's permissions. Additional agent options (e.g.
|
|
254
|
+
# +permissions: :readwrite+) may be passed through.
|
|
255
|
+
# @param opts [Hash] extra keyword args forwarded to {Parse::Agent#initialize}.
|
|
256
|
+
# @return [Parse::Agent, nil] +nil+ when the payload carried no token.
|
|
257
|
+
def user_agent(**opts)
|
|
258
|
+
return nil if @session_token.nil?
|
|
259
|
+
require_relative "../agent" unless defined?(Parse::Agent)
|
|
260
|
+
# Strip the two identity kwargs from the passthrough: a Ruby double-splat
|
|
261
|
+
# that repeats an explicit keyword WINS, so user_agent(client: master)
|
|
262
|
+
# or user_agent(session_token: other) would otherwise silently defeat the
|
|
263
|
+
# whole point (scoping to the caller). The scoping is non-negotiable here.
|
|
264
|
+
opts = opts.except(:session_token, :client)
|
|
265
|
+
Parse::Agent.new(session_token: @session_token, client: user_client, **opts)
|
|
266
|
+
end
|
|
267
|
+
|
|
173
268
|
# @return [String] the name of the Parse class for this request.
|
|
174
269
|
def parse_class
|
|
175
270
|
return @webhook_class if @webhook_class.present?
|
|
@@ -321,9 +416,57 @@ module Parse
|
|
|
321
416
|
return o
|
|
322
417
|
end
|
|
323
418
|
|
|
324
|
-
# afterSave on a CREATE
|
|
325
|
-
#
|
|
326
|
-
#
|
|
419
|
+
# afterSave on a CREATE: there is no prior persisted state, so every
|
|
420
|
+
# populated data field is new. Build symmetry with the UPDATE path above
|
|
421
|
+
# by seeding the identity / system fields (objectId, timestamps, ACL,
|
|
422
|
+
# className, plus the credential / permission keys) into a pristine
|
|
423
|
+
# object, then overlaying the full object with dirty tracking. Because
|
|
424
|
+
# the overlay's protected_set is exactly the seed key set, the system
|
|
425
|
+
# fields come ONLY from the clean seed and the overlay touches ONLY
|
|
426
|
+
# declared data properties (nil -> value -> changed) -- so `*_changed?`
|
|
427
|
+
# reports every field the create populated while createdAt / updatedAt /
|
|
428
|
+
# ACL stay clean. This lets handlers key off dirty tracking uniformly
|
|
429
|
+
# across create and update (e.g. building a sync payload from changed
|
|
430
|
+
# fields). Credentials / _rperm / _wperm / authData / roles are filtered
|
|
431
|
+
# from the overlay (seeded read-only, never marked changed), and an
|
|
432
|
+
# after-trigger response is only true/false, so nothing here can persist
|
|
433
|
+
# a forged field.
|
|
434
|
+
if after_save? && @object.is_a?(Hash)
|
|
435
|
+
seed_keys = Parse::Properties::PROTECTED_INITIALIZE_KEYS +
|
|
436
|
+
%w[objectId createdAt updatedAt ACL className __type]
|
|
437
|
+
seed = @object.slice(*seed_keys)
|
|
438
|
+
o = Parse::Object.build seed, parse_class
|
|
439
|
+
# `build` applies declared `default:` values onto the seed and then
|
|
440
|
+
# clears changes, baking each default into the "pristine" baseline.
|
|
441
|
+
# Without correction, the overlay's dirty guard (`unless val ==
|
|
442
|
+
# current`) would SUPPRESS marking any create value equal to its
|
|
443
|
+
# default (e.g. status: "draft", count: 0, archived: false), silently
|
|
444
|
+
# defeating this branch's whole purpose. Reset the default-bearing
|
|
445
|
+
# ivars to nil for the fields the overlay is about to set, then
|
|
446
|
+
# re-clear, so the overlay's guard sees a differing current ivar and
|
|
447
|
+
# marks every populated data field changed. (`*_changed?` / `changes`
|
|
448
|
+
# / `changed` then report the field; for a defaulted field the
|
|
449
|
+
# reported prior value is the default rather than nil, since the
|
|
450
|
+
# getter re-derives the default — its prior *effective* value.)
|
|
451
|
+
# `defaults_list` never contains the seeded system fields
|
|
452
|
+
# (objectId/createdAt/updatedAt/ACL), so this cannot disturb them;
|
|
453
|
+
# defaults for fields ABSENT from the payload are left intact so their
|
|
454
|
+
# value still reads through.
|
|
455
|
+
fmap = o.class.respond_to?(:field_map) ? o.class.field_map : {}
|
|
456
|
+
o.class.defaults_list.each do |k|
|
|
457
|
+
wire = (fmap[k] || k).to_s
|
|
458
|
+
next unless @object.key?(wire) || @object.key?(k.to_s)
|
|
459
|
+
o.instance_variable_set(:"@#{k}", nil)
|
|
460
|
+
end
|
|
461
|
+
o.clear_changes!
|
|
462
|
+
o.apply_attributes! @object, dirty_track: true, protected_set: seed_keys
|
|
463
|
+
return o
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
# Every other trigger (afterDelete, afterFind, and any before* path that
|
|
467
|
+
# did not match above): the full object as the server sent it.
|
|
468
|
+
# createdAt/updatedAt survive (only credentials are scrubbed), so
|
|
469
|
+
# `new?` / `existed?` read correctly.
|
|
327
470
|
Parse::Object.build(@object, parse_class)
|
|
328
471
|
end
|
|
329
472
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: parse-stack-next
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 5.
|
|
4
|
+
version: 5.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Adrian Curtin
|
|
@@ -364,6 +364,7 @@ files:
|
|
|
364
364
|
- lib/parse/model/core/field_guards.rb
|
|
365
365
|
- lib/parse/model/core/indexing.rb
|
|
366
366
|
- lib/parse/model/core/parse_reference.rb
|
|
367
|
+
- lib/parse/model/core/pluralized_aliases.rb
|
|
367
368
|
- lib/parse/model/core/properties.rb
|
|
368
369
|
- lib/parse/model/core/querying.rb
|
|
369
370
|
- lib/parse/model/core/schema.rb
|