parse-stack-next 4.5.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 +7 -0
- data/.bundle/config +2 -0
- data/.env.sample +112 -0
- data/.env.test +10 -0
- data/.github/workflows/ruby.yml +36 -0
- data/.gitignore +49 -0
- data/.ruby-version +1 -0
- data/.solargraph.yml +22 -0
- data/CHANGELOG.md +5816 -0
- data/Gemfile +30 -0
- data/Gemfile.lock +175 -0
- data/LICENSE.txt +23 -0
- data/Makefile +63 -0
- data/README.md +5655 -0
- data/Rakefile +573 -0
- data/bin/console +38 -0
- data/bin/parse-console +136 -0
- data/bin/server +17 -0
- data/bin/setup +7 -0
- data/config/parse-config.json +12 -0
- data/docs/TEST_SERVER.md +271 -0
- data/docs/_config.yml +1 -0
- data/docs/mcp_guide.md +3484 -0
- data/docs/mongodb_direct_guide.md +1348 -0
- data/docs/mongodb_index_optimization_guide.md +631 -0
- data/examples/transaction_example.rb +219 -0
- data/lib/parse/acl_scope.rb +728 -0
- data/lib/parse/agent/cancellation_token.rb +80 -0
- data/lib/parse/agent/constraint_translator.rb +480 -0
- data/lib/parse/agent/describe.rb +420 -0
- data/lib/parse/agent/errors.rb +133 -0
- data/lib/parse/agent/mcp_client.rb +557 -0
- data/lib/parse/agent/mcp_dispatcher.rb +1023 -0
- data/lib/parse/agent/mcp_rack_app.rb +1143 -0
- data/lib/parse/agent/mcp_server.rb +376 -0
- data/lib/parse/agent/metadata_audit.rb +259 -0
- data/lib/parse/agent/metadata_dsl.rb +733 -0
- data/lib/parse/agent/metadata_registry.rb +794 -0
- data/lib/parse/agent/pipeline_validator.rb +82 -0
- data/lib/parse/agent/prompts.rb +351 -0
- data/lib/parse/agent/rate_limiter.rb +158 -0
- data/lib/parse/agent/relation_graph.rb +162 -0
- data/lib/parse/agent/result_formatter.rb +453 -0
- data/lib/parse/agent/tools.rb +5489 -0
- data/lib/parse/agent.rb +3249 -0
- data/lib/parse/api/aggregate.rb +79 -0
- data/lib/parse/api/all.rb +26 -0
- data/lib/parse/api/analytics.rb +18 -0
- data/lib/parse/api/batch.rb +33 -0
- data/lib/parse/api/cloud_functions.rb +58 -0
- data/lib/parse/api/config.rb +125 -0
- data/lib/parse/api/files.rb +29 -0
- data/lib/parse/api/hooks.rb +117 -0
- data/lib/parse/api/objects.rb +146 -0
- data/lib/parse/api/path_segment.rb +75 -0
- data/lib/parse/api/push.rb +20 -0
- data/lib/parse/api/schema.rb +49 -0
- data/lib/parse/api/server.rb +50 -0
- data/lib/parse/api/sessions.rb +24 -0
- data/lib/parse/api/users.rb +250 -0
- data/lib/parse/atlas_search/index_manager.rb +353 -0
- data/lib/parse/atlas_search/result.rb +204 -0
- data/lib/parse/atlas_search/search_builder.rb +604 -0
- data/lib/parse/atlas_search/session.rb +253 -0
- data/lib/parse/atlas_search.rb +995 -0
- data/lib/parse/client/authentication.rb +97 -0
- data/lib/parse/client/batch.rb +234 -0
- data/lib/parse/client/body_builder.rb +240 -0
- data/lib/parse/client/caching.rb +203 -0
- data/lib/parse/client/logging.rb +293 -0
- data/lib/parse/client/profiling.rb +181 -0
- data/lib/parse/client/protocol.rb +91 -0
- data/lib/parse/client/request.rb +233 -0
- data/lib/parse/client/response.rb +208 -0
- data/lib/parse/client.rb +1104 -0
- data/lib/parse/clp_scope.rb +361 -0
- data/lib/parse/live_query/circuit_breaker.rb +256 -0
- data/lib/parse/live_query/client.rb +1001 -0
- data/lib/parse/live_query/configuration.rb +224 -0
- data/lib/parse/live_query/event.rb +115 -0
- data/lib/parse/live_query/event_queue.rb +272 -0
- data/lib/parse/live_query/health_monitor.rb +214 -0
- data/lib/parse/live_query/logging.rb +149 -0
- data/lib/parse/live_query/subscription.rb +294 -0
- data/lib/parse/live_query.rb +163 -0
- data/lib/parse/lookup_rewriter.rb +445 -0
- data/lib/parse/model/acl.rb +968 -0
- data/lib/parse/model/associations/belongs_to.rb +275 -0
- data/lib/parse/model/associations/collection_proxy.rb +435 -0
- data/lib/parse/model/associations/has_many.rb +597 -0
- data/lib/parse/model/associations/has_one.rb +158 -0
- data/lib/parse/model/associations/pointer_collection_proxy.rb +134 -0
- data/lib/parse/model/associations/relation_collection_proxy.rb +177 -0
- data/lib/parse/model/bytes.rb +62 -0
- data/lib/parse/model/classes/audience.rb +262 -0
- data/lib/parse/model/classes/installation.rb +363 -0
- data/lib/parse/model/classes/job_schedule.rb +153 -0
- data/lib/parse/model/classes/job_status.rb +264 -0
- data/lib/parse/model/classes/product.rb +75 -0
- data/lib/parse/model/classes/push_status.rb +263 -0
- data/lib/parse/model/classes/role.rb +751 -0
- data/lib/parse/model/classes/session.rb +201 -0
- data/lib/parse/model/classes/user.rb +943 -0
- data/lib/parse/model/clp.rb +544 -0
- data/lib/parse/model/core/actions.rb +1268 -0
- data/lib/parse/model/core/builder.rb +139 -0
- data/lib/parse/model/core/create_lock.rb +386 -0
- data/lib/parse/model/core/describe.rb +382 -0
- data/lib/parse/model/core/enhanced_change_tracking.rb +159 -0
- data/lib/parse/model/core/errors.rb +38 -0
- data/lib/parse/model/core/fetching.rb +566 -0
- data/lib/parse/model/core/field_guards.rb +220 -0
- data/lib/parse/model/core/indexing.rb +382 -0
- data/lib/parse/model/core/parse_reference.rb +407 -0
- data/lib/parse/model/core/properties.rb +809 -0
- data/lib/parse/model/core/querying.rb +491 -0
- data/lib/parse/model/core/schema.rb +202 -0
- data/lib/parse/model/core/search_indexing.rb +174 -0
- data/lib/parse/model/date.rb +88 -0
- data/lib/parse/model/email.rb +213 -0
- data/lib/parse/model/file.rb +527 -0
- data/lib/parse/model/geojson.rb +271 -0
- data/lib/parse/model/geopoint.rb +261 -0
- data/lib/parse/model/model.rb +260 -0
- data/lib/parse/model/object.rb +2068 -0
- data/lib/parse/model/phone.rb +520 -0
- data/lib/parse/model/pointer.rb +443 -0
- data/lib/parse/model/polygon.rb +406 -0
- data/lib/parse/model/push.rb +975 -0
- data/lib/parse/model/shortnames.rb +8 -0
- data/lib/parse/model/time_zone.rb +141 -0
- data/lib/parse/model/validations/uniqueness_validator.rb +97 -0
- data/lib/parse/model/validations.rb +96 -0
- data/lib/parse/mongodb.rb +2300 -0
- data/lib/parse/pipeline_security.rb +554 -0
- data/lib/parse/query/constraint.rb +198 -0
- data/lib/parse/query/constraints.rb +3279 -0
- data/lib/parse/query/cursor.rb +434 -0
- data/lib/parse/query/n_plus_one_detector.rb +445 -0
- data/lib/parse/query/operation.rb +104 -0
- data/lib/parse/query/ordering.rb +66 -0
- data/lib/parse/query.rb +7028 -0
- data/lib/parse/schema/index_migrator.rb +291 -0
- data/lib/parse/schema/search_index_migrator.rb +289 -0
- data/lib/parse/schema.rb +494 -0
- data/lib/parse/stack/generators/rails.rb +40 -0
- data/lib/parse/stack/generators/templates/model.erb +51 -0
- data/lib/parse/stack/generators/templates/model_installation.rb +4 -0
- data/lib/parse/stack/generators/templates/model_role.rb +4 -0
- data/lib/parse/stack/generators/templates/model_session.rb +4 -0
- data/lib/parse/stack/generators/templates/model_user.rb +11 -0
- data/lib/parse/stack/generators/templates/parse.rb +12 -0
- data/lib/parse/stack/generators/templates/webhooks.rb +10 -0
- data/lib/parse/stack/railtie.rb +18 -0
- data/lib/parse/stack/tasks.rb +563 -0
- data/lib/parse/stack/version.rb +11 -0
- data/lib/parse/stack.rb +455 -0
- data/lib/parse/two_factor_auth/user_extension.rb +449 -0
- data/lib/parse/two_factor_auth.rb +310 -0
- data/lib/parse/webhooks/payload.rb +360 -0
- data/lib/parse/webhooks/registration.rb +199 -0
- data/lib/parse/webhooks/replay_protection.rb +189 -0
- data/lib/parse/webhooks.rb +510 -0
- data/lib/parse-stack-next.rb +5 -0
- data/lib/parse-stack.rb +5 -0
- data/parse-stack-next.gemspec +82 -0
- data/parse-stack.png +0 -0
- data/scripts/debug-ips.js +35 -0
- data/scripts/docker/Dockerfile.parse +13 -0
- data/scripts/docker/atlas-init.js +284 -0
- data/scripts/docker/docker-compose.atlas.yml +76 -0
- data/scripts/docker/docker-compose.test.yml +106 -0
- data/scripts/docker/mongo-init.js +21 -0
- data/scripts/eval_mcp_with_lm_studio.rb +274 -0
- data/scripts/start-parse.sh +90 -0
- data/scripts/start_mcp_server.rb +78 -0
- data/scripts/test_server_connection.rb +82 -0
- metadata +377 -0
|
@@ -0,0 +1,809 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "active_model"
|
|
5
|
+
require "active_support"
|
|
6
|
+
require "active_support/inflector"
|
|
7
|
+
require "active_support/core_ext"
|
|
8
|
+
require "active_support/core_ext/object"
|
|
9
|
+
require "active_support/inflector"
|
|
10
|
+
require "active_model/serializers/json"
|
|
11
|
+
require "active_support/inflector"
|
|
12
|
+
require "active_model/serializers/json"
|
|
13
|
+
require "active_support/hash_with_indifferent_access"
|
|
14
|
+
require "time"
|
|
15
|
+
|
|
16
|
+
module Parse
|
|
17
|
+
|
|
18
|
+
# This module provides support for handling all the different types of column data types
|
|
19
|
+
# supported in Parse and mapping them between their remote names with their local ruby named attributes.
|
|
20
|
+
module Properties
|
|
21
|
+
# These are the base types supported by Parse.
|
|
22
|
+
TYPES = [:string, :relation, :integer, :float, :boolean, :date, :array, :file, :geopoint, :polygon, :bytes, :object, :acl, :timezone, :phone, :email].freeze
|
|
23
|
+
# These are the base mappings of the remote field name types.
|
|
24
|
+
BASE = { objectId: :string, createdAt: :date, updatedAt: :date, ACL: :acl }.freeze
|
|
25
|
+
# The list of properties that are part of all objects
|
|
26
|
+
BASE_KEYS = [:id, :created_at, :updated_at].freeze
|
|
27
|
+
# Attribute names refused on the mass-assignment path
|
|
28
|
+
# (`Parse::Object#attributes=` and `apply_attributes!` with
|
|
29
|
+
# `dirty_track: true`). Internal hydration from server responses uses
|
|
30
|
+
# `dirty_track: false` and is unaffected, so server-issued
|
|
31
|
+
# sessionTokens etc. still flow through during decoding.
|
|
32
|
+
#
|
|
33
|
+
# The list intentionally covers ONLY server-managed and security-
|
|
34
|
+
# internal fields. User-facing properties like `acl` and `objectId`
|
|
35
|
+
# are deliberately omitted because constructor calls like
|
|
36
|
+
# `Document.new(acl: my_acl)` are legitimate developer code. Rails
|
|
37
|
+
# applications receiving form input should use StrongParameters
|
|
38
|
+
# (`params.permit(...)`) to filter attacker-controlled keys before
|
|
39
|
+
# passing the hash to `Model.new` or `attributes=`.
|
|
40
|
+
PROTECTED_MASS_ASSIGNMENT_KEYS = %w[
|
|
41
|
+
sessionToken session_token
|
|
42
|
+
roles _rperm _wperm
|
|
43
|
+
_hashed_password _password_history
|
|
44
|
+
authData _auth_data auth_data
|
|
45
|
+
className __type
|
|
46
|
+
createdAt created_at updatedAt updated_at
|
|
47
|
+
].freeze
|
|
48
|
+
# Narrow subset of {PROTECTED_MASS_ASSIGNMENT_KEYS} that closes the
|
|
49
|
+
# documented authentication / authorization mass-assignment attacks
|
|
50
|
+
# (NEW-EXT-1) without breaking the legitimate "build a hydrated
|
|
51
|
+
# object" pattern (`Klass.new("objectId" => id, "createdAt" => ts,
|
|
52
|
+
# "field" => …)`). Applied by `Parse::Object#initialize` when
|
|
53
|
+
# `trusted: false` (the default) so caller-supplied hashes — even
|
|
54
|
+
# those bearing an `objectId` — cannot forge session tokens, ACL
|
|
55
|
+
# row-permissions, password hashes, OAuth auth_data, or roles.
|
|
56
|
+
#
|
|
57
|
+
# Excluded from this narrow set on purpose:
|
|
58
|
+
# - `createdAt` / `updatedAt`: timestamp integrity, not a security
|
|
59
|
+
# boundary. App code commonly rehydrates cached objects via
|
|
60
|
+
# `Klass.new(hash)` and expects timestamps to populate.
|
|
61
|
+
# - `className` / `__type`: routing metadata. `Parse::Object.build`
|
|
62
|
+
# has its own className-mismatch guard; the in-memory value here
|
|
63
|
+
# is informational only.
|
|
64
|
+
#
|
|
65
|
+
# The wider {PROTECTED_MASS_ASSIGNMENT_KEYS} list still applies to
|
|
66
|
+
# `Parse::Object#attributes=` and explicit
|
|
67
|
+
# `apply_attributes!(dirty_track: true)` calls, where Rails-form
|
|
68
|
+
# input is the expected source and timestamp forgery is also
|
|
69
|
+
# undesirable.
|
|
70
|
+
PROTECTED_INITIALIZE_KEYS = %w[
|
|
71
|
+
sessionToken session_token
|
|
72
|
+
roles _rperm _wperm
|
|
73
|
+
_hashed_password _password_history
|
|
74
|
+
authData _auth_data auth_data
|
|
75
|
+
].freeze
|
|
76
|
+
# Default hash map of local attribute name to remote column name
|
|
77
|
+
BASE_FIELD_MAP = { id: :objectId, created_at: :createdAt, updated_at: :updatedAt, acl: :ACL }.freeze
|
|
78
|
+
# The delete operation hash.
|
|
79
|
+
CORE_FIELDS = { id: :string, created_at: :date, updated_at: :date, acl: :acl }.freeze
|
|
80
|
+
# The delete operation hash.
|
|
81
|
+
DELETE_OP = { "__op" => "Delete" }.freeze
|
|
82
|
+
# @!visibility private
|
|
83
|
+
def self.included(base)
|
|
84
|
+
base.extend(ClassMethods)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# The class methods added to Parse::Objects
|
|
88
|
+
module ClassMethods
|
|
89
|
+
|
|
90
|
+
# The fields method returns a mapping of all local attribute names and their data type.
|
|
91
|
+
# if type is passed, we return only the fields that matched that data type. If `type`
|
|
92
|
+
# is provided, it will only return the fields that match the data type.
|
|
93
|
+
# @param type [Symbol] a property type.
|
|
94
|
+
# @return [Hash] the defined fields for this Parse collection with their data type.
|
|
95
|
+
def fields(type = nil)
|
|
96
|
+
# if it's Parse::Object, then only use the initial set, otherwise add the other base fields.
|
|
97
|
+
@fields ||= (self == Parse::Object ? CORE_FIELDS : Parse::Object.fields).dup
|
|
98
|
+
if type.present?
|
|
99
|
+
type = type.to_sym
|
|
100
|
+
return @fields.select { |k, v| v == type }
|
|
101
|
+
end
|
|
102
|
+
@fields
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# @return [Hash] the field map for this subclass.
|
|
106
|
+
def field_map
|
|
107
|
+
@field_map ||= BASE_FIELD_MAP.dup
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# @return [Hash] the fields that are marked as enums.
|
|
111
|
+
def enums
|
|
112
|
+
@enums ||= {}
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# @return [Hash] semantic descriptions for properties (used by Parse::Agent).
|
|
116
|
+
# Maps property names (symbols) to their description strings.
|
|
117
|
+
def property_descriptions
|
|
118
|
+
@property_descriptions ||= {}
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# @return [Hash] per-value descriptions for enum-shaped string
|
|
122
|
+
# properties (used by Parse::Agent). Maps property names (symbols)
|
|
123
|
+
# to a `{ "value" => "description" }` hash. Orthogonal to the
|
|
124
|
+
# existing `enum:` option on `property` — `enum:` validates the
|
|
125
|
+
# set of allowed values, `_enum:` documents each one for an LLM.
|
|
126
|
+
#
|
|
127
|
+
# **Intended for string-typed columns only.** Value keys are
|
|
128
|
+
# stringified at declaration time and the schema response carries
|
|
129
|
+
# `{value: "1", ...}` regardless of the underlying column type.
|
|
130
|
+
# Declaring `_enum:` on an integer/boolean column will surface
|
|
131
|
+
# string-shaped values to the LLM that won't match the column
|
|
132
|
+
# in a `where:` filter — userland is responsible for keeping
|
|
133
|
+
# `_enum:` on string-typed properties.
|
|
134
|
+
def property_enum_descriptions
|
|
135
|
+
@property_enum_descriptions ||= {}
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Set the property fields for this class.
|
|
139
|
+
# @return [Hash]
|
|
140
|
+
def attributes=(hash)
|
|
141
|
+
@attributes = BASE.merge(hash)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# @return [Hash] the fields that are marked as enums.
|
|
145
|
+
def attributes
|
|
146
|
+
@attributes ||= BASE.dup
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# @return [Array] the list of fields that have defaults.
|
|
150
|
+
def defaults_list
|
|
151
|
+
@defaults_list ||= []
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# property :songs, :array
|
|
155
|
+
# property :my_date, :date, field: "myRemoteCOLUMNName"
|
|
156
|
+
# property :my_int, :integer, required: true, default: ->{ rand(10) }
|
|
157
|
+
|
|
158
|
+
# field: (literal column name in Parse)
|
|
159
|
+
# required: (data_type)
|
|
160
|
+
# default: (value or Proc)
|
|
161
|
+
# alias: Whether to create the remote field alias getter/setters for this attribute
|
|
162
|
+
|
|
163
|
+
# symbolize: Makes sure the saved and return value locally is in symbol format. useful
|
|
164
|
+
# for enum type fields that are string columns in Parse. Ex. a booking_status for a field
|
|
165
|
+
# could be either "submitted" or "completed" in Parse, however with symbolize, these would
|
|
166
|
+
# be available as :submitted or :completed.
|
|
167
|
+
|
|
168
|
+
# This is the class level property method to be used when declaring properties. This helps builds specific methods, formatters
|
|
169
|
+
# and conversion handlers for property storing and saving data for a particular parse class.
|
|
170
|
+
# The first parameter is the name of the local attribute you want to declare with its corresponding data type.
|
|
171
|
+
# Declaring a `property :my_date, :date`, would declare the attribute my_date with a corresponding remote column called
|
|
172
|
+
# "myDate" (lower-first-camelcase) with a Parse data type of Date.
|
|
173
|
+
# You can override the implicit naming behavior by passing the option :field to override.
|
|
174
|
+
def property(key, data_type = :string, **opts)
|
|
175
|
+
key = key.to_sym
|
|
176
|
+
ivar = :"@#{key}"
|
|
177
|
+
will_change_method = :"#{key}_will_change!"
|
|
178
|
+
set_attribute_method = :"#{key}_set_attribute!"
|
|
179
|
+
|
|
180
|
+
if data_type.is_a?(Hash)
|
|
181
|
+
opts.merge!(data_type)
|
|
182
|
+
data_type = :string
|
|
183
|
+
# future: automatically use :timezone datatype for timezone-like fields.
|
|
184
|
+
# when the data_type was not specifically set.
|
|
185
|
+
# data_type = :timezone if key == :time_zone || key == :timezone
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
data_type = :timezone if data_type == :string && (key == :time_zone || key == :timezone)
|
|
189
|
+
|
|
190
|
+
# allow :bool for :boolean
|
|
191
|
+
data_type = :boolean if data_type == :bool
|
|
192
|
+
data_type = :timezone if data_type == :time_zone
|
|
193
|
+
data_type = :geopoint if data_type == :geo_point
|
|
194
|
+
data_type = :polygon if data_type == :geo_polygon
|
|
195
|
+
data_type = :integer if data_type == :int || data_type == :number
|
|
196
|
+
data_type = :phone if data_type == :phone_number || data_type == :mobile || data_type == :e164
|
|
197
|
+
data_type = :email if data_type == :email_address
|
|
198
|
+
|
|
199
|
+
# set defaults
|
|
200
|
+
opts = { required: false,
|
|
201
|
+
alias: true,
|
|
202
|
+
symbolize: false,
|
|
203
|
+
enum: nil,
|
|
204
|
+
scopes: true,
|
|
205
|
+
_prefix: nil,
|
|
206
|
+
_suffix: false,
|
|
207
|
+
_description: nil, # Agent metadata: semantic description for LLMs
|
|
208
|
+
_enum: nil, # Agent metadata: per-value enum descriptions ({ value => description })
|
|
209
|
+
field: key.to_s.camelize(:lower) }.merge(opts)
|
|
210
|
+
#By default, the remote field name is a lower-first-camelcase version of the key
|
|
211
|
+
# it can be overriden by the :field parameter
|
|
212
|
+
parse_field = opts[:field].to_sym
|
|
213
|
+
# If this property is already defined (either as a custom property on this class or as a
|
|
214
|
+
# core property on a Parse::Object subclass), decide whether to silently apply non-structural
|
|
215
|
+
# updates, raise, or warn-and-drop. Structural changes (different data type or different
|
|
216
|
+
# remote field name) are almost always bugs — like declaring Installation#badge as :string
|
|
217
|
+
# when the server stores it as :integer — so they raise when Parse.strict_property_redefinition
|
|
218
|
+
# is enabled (the default). Non-structural redeclarations (same type, same remote field) are
|
|
219
|
+
# allowed and may refine metadata such as :default, :_description, and :_enum without warning;
|
|
220
|
+
# this covers class reopens that re-affirm an existing property after a parse-stack upgrade
|
|
221
|
+
# adds the same definition upstream, or that bolt a default value onto an inherited field.
|
|
222
|
+
if (self.fields[key].present? && BASE_FIELD_MAP[key].nil?) || (self < Parse::Object && BASE_FIELD_MAP.has_key?(key))
|
|
223
|
+
existing_type = self.fields[key]
|
|
224
|
+
existing_parse_field = self.field_map[key]
|
|
225
|
+
if existing_type == data_type && existing_parse_field == parse_field
|
|
226
|
+
# Non-structural redeclaration: apply safe metadata-only updates and bail out before
|
|
227
|
+
# the rest of the method redefines getters/setters/validations/scopes.
|
|
228
|
+
if opts.key?(:default)
|
|
229
|
+
default_value = opts[:default]
|
|
230
|
+
defaults_list.push(key) unless defaults_list.include?(key)
|
|
231
|
+
define_method("#{key}_default") do
|
|
232
|
+
default_value.is_a?(Proc) ? default_value.call(self) : default_value
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
if opts[:_description].present?
|
|
236
|
+
self.property_descriptions[key] = opts[:_description].to_s.freeze
|
|
237
|
+
end
|
|
238
|
+
if opts[:_enum].is_a?(Hash) && opts[:_enum].any?
|
|
239
|
+
normalized = opts[:_enum].each_with_object({}) do |(value, desc), h|
|
|
240
|
+
h[value.to_s] = desc.to_s.freeze
|
|
241
|
+
end
|
|
242
|
+
self.property_enum_descriptions[key] = normalized.freeze
|
|
243
|
+
end
|
|
244
|
+
return true
|
|
245
|
+
end
|
|
246
|
+
if Parse.strict_property_redefinition
|
|
247
|
+
raise ArgumentError,
|
|
248
|
+
"Property #{self}##{key} is already defined as :#{existing_type} " \
|
|
249
|
+
"(remote field :#{existing_parse_field}); refusing to redeclare as :#{data_type} " \
|
|
250
|
+
"(remote field :#{parse_field}). Set Parse.strict_property_redefinition = false " \
|
|
251
|
+
"to fall back to warn-and-ignore behavior."
|
|
252
|
+
end
|
|
253
|
+
warn "Property #{self}##{key} already defined with data type :#{data_type}. Will be ignored."
|
|
254
|
+
return false
|
|
255
|
+
end
|
|
256
|
+
# We keep the list of fields that are on the remote Parse store
|
|
257
|
+
if self.fields[parse_field].present? || (self < Parse::Object && BASE.has_key?(parse_field))
|
|
258
|
+
warn "Alias property #{self}##{parse_field} conflicts with previously defined property. Will be ignored."
|
|
259
|
+
return false
|
|
260
|
+
# raise ArgumentError
|
|
261
|
+
end
|
|
262
|
+
#dirty tracking. It is declared to use with ActiveModel DirtyTracking
|
|
263
|
+
define_attribute_methods key
|
|
264
|
+
|
|
265
|
+
# this hash keeps list of attributes (based on remote fields) and their data types
|
|
266
|
+
self.attributes.merge!(parse_field => data_type)
|
|
267
|
+
# this maps all the possible attribute fields and their data types. We use both local
|
|
268
|
+
# keys and remote keys because when we receive a remote object that has the remote field name
|
|
269
|
+
# we need to know what the data type conversion should be.
|
|
270
|
+
self.fields.merge!(key => data_type, parse_field => data_type)
|
|
271
|
+
# This creates a mapping between the local field and the remote field name.
|
|
272
|
+
self.field_map.merge!(key => parse_field)
|
|
273
|
+
|
|
274
|
+
# Store the property description for agent metadata if provided
|
|
275
|
+
if opts[:_description].present?
|
|
276
|
+
self.property_descriptions[key] = opts[:_description].to_s.freeze
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Store per-value enum descriptions for agent metadata if provided.
|
|
280
|
+
# Accepts a Hash mapping each allowed value (Symbol or String) to a
|
|
281
|
+
# description string. Stored with stringified value keys to match the
|
|
282
|
+
# wire-format shape an LLM will see in query constraints. Distinct
|
|
283
|
+
# from the existing `enum:` option, which is a validation construct.
|
|
284
|
+
if opts[:_enum].is_a?(Hash) && opts[:_enum].any?
|
|
285
|
+
normalized = opts[:_enum].each_with_object({}) do |(value, desc), h|
|
|
286
|
+
h[value.to_s] = desc.to_s.freeze
|
|
287
|
+
end
|
|
288
|
+
self.property_enum_descriptions[key] = normalized.freeze
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# if the field is marked as required, then add validations
|
|
292
|
+
if opts[:required]
|
|
293
|
+
# if integer or float, validate that it's a number
|
|
294
|
+
if data_type == :integer || data_type == :float
|
|
295
|
+
validates_numericality_of key
|
|
296
|
+
end
|
|
297
|
+
# validate that it is not empty
|
|
298
|
+
validates_presence_of key
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# timezone datatypes are basically enums based on IANA time zone identifiers.
|
|
302
|
+
if data_type == :timezone
|
|
303
|
+
validates_each key do |record, attribute, value|
|
|
304
|
+
# Parse::TimeZone objects have a `valid?` method to determine if the timezone is valid.
|
|
305
|
+
unless value.nil? || value.valid?
|
|
306
|
+
record.errors.add(attribute, "field :#{attribute} must be a valid IANA time zone identifier.")
|
|
307
|
+
end
|
|
308
|
+
end # validates_each
|
|
309
|
+
end # data_type == :timezone
|
|
310
|
+
|
|
311
|
+
# phone datatypes validate E.164 format.
|
|
312
|
+
if data_type == :phone
|
|
313
|
+
validates_each key do |record, attribute, value|
|
|
314
|
+
# Parse::Phone objects have a `valid?` method to determine if the phone is valid E.164.
|
|
315
|
+
unless value.nil? || value.valid?
|
|
316
|
+
record.errors.add(attribute, "field :#{attribute} must be a valid E.164 phone number (e.g., +14155551234).")
|
|
317
|
+
end
|
|
318
|
+
end # validates_each
|
|
319
|
+
end # data_type == :phone
|
|
320
|
+
|
|
321
|
+
# email datatypes validate email format.
|
|
322
|
+
if data_type == :email
|
|
323
|
+
validates_each key do |record, attribute, value|
|
|
324
|
+
# Parse::Email objects have a `valid?` method to determine if the email is valid.
|
|
325
|
+
unless value.nil? || value.valid?
|
|
326
|
+
record.errors.add(attribute, "field :#{attribute} must be a valid email address.")
|
|
327
|
+
end
|
|
328
|
+
end # validates_each
|
|
329
|
+
end # data_type == :email
|
|
330
|
+
|
|
331
|
+
is_enum_type = opts[:enum].nil? == false
|
|
332
|
+
|
|
333
|
+
if is_enum_type
|
|
334
|
+
unless data_type == :string
|
|
335
|
+
raise ArgumentError, "Property #{self}##{parse_field} :enum option is only supported on :string data types."
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
enum_values = opts[:enum]
|
|
339
|
+
unless enum_values.is_a?(Array) && enum_values.empty? == false
|
|
340
|
+
raise ArgumentError, "Property #{self}##{parse_field} :enum option must be an Array type of symbols."
|
|
341
|
+
end
|
|
342
|
+
opts[:symbolize] = true
|
|
343
|
+
|
|
344
|
+
enum_values = enum_values.dup.map(&:to_sym).freeze
|
|
345
|
+
|
|
346
|
+
self.enums.merge!(key => enum_values)
|
|
347
|
+
allow_nil = opts[:required] == false
|
|
348
|
+
validates key, inclusion: { in: enum_values }, allow_nil: allow_nil
|
|
349
|
+
|
|
350
|
+
unless opts[:scopes] == false
|
|
351
|
+
# You can use the :_prefix or :_suffix options when you need to define multiple enums with same values.
|
|
352
|
+
# If the passed value is true, the methods are prefixed/suffixed with the name of the enum. It is also possible to supply a custom value:
|
|
353
|
+
prefix = opts[:_prefix]
|
|
354
|
+
unless opts[:_prefix].nil? || prefix.is_a?(Symbol) || prefix.is_a?(String)
|
|
355
|
+
raise ArgumentError, "Enumeration option :_prefix must either be a symbol or string for #{self}##{key}."
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
unless opts[:_suffix].is_a?(TrueClass) || opts[:_suffix].is_a?(FalseClass)
|
|
359
|
+
raise ArgumentError, "Enumeration option :_suffix must either be true or false for #{self}##{key}."
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
add_suffix = opts[:_suffix] == true
|
|
363
|
+
prefix_or_key = (prefix.blank? ? key : prefix).to_sym
|
|
364
|
+
|
|
365
|
+
class_method_name = prefix_or_key.to_s.pluralize.to_sym
|
|
366
|
+
if singleton_class.method_defined?(class_method_name)
|
|
367
|
+
raise ArgumentError, "You tried to define an enum named `#{key}` for #{self} " + "but this will generate a method `#{self}.#{class_method_name}` " + " which is already defined. Try using :_suffix or :_prefix options."
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
define_singleton_method(class_method_name) { enum_values }
|
|
371
|
+
|
|
372
|
+
method_name = add_suffix ? :"valid_#{prefix_or_key}?" : :"#{prefix_or_key}_valid?"
|
|
373
|
+
define_method(method_name) do
|
|
374
|
+
value = send(key) # call default getter
|
|
375
|
+
return true if allow_nil && value.nil?
|
|
376
|
+
enum_values.include?(value.to_s.to_sym)
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
enum_values.each do |enum|
|
|
380
|
+
method_name = enum # default
|
|
381
|
+
if add_suffix
|
|
382
|
+
method_name = :"#{enum}_#{prefix_or_key}"
|
|
383
|
+
elsif prefix.present?
|
|
384
|
+
method_name = :"#{prefix}_#{enum}"
|
|
385
|
+
end
|
|
386
|
+
self.scope method_name, ->(ex = {}) { ex.merge!(key => enum); query(ex) }
|
|
387
|
+
|
|
388
|
+
define_method("#{method_name}!") { send set_attribute_method, enum, true }
|
|
389
|
+
define_method("#{method_name}?") { enum == send(key).to_s.to_sym }
|
|
390
|
+
end
|
|
391
|
+
end # unless scopes
|
|
392
|
+
end # if is enum
|
|
393
|
+
|
|
394
|
+
symbolize_value = opts[:symbolize]
|
|
395
|
+
|
|
396
|
+
#only support symbolization of string data types
|
|
397
|
+
if symbolize_value && (data_type == :string || data_type == :array) == false
|
|
398
|
+
raise ArgumentError, "Tried to symbolize #{self}##{key}, but it is only supported on :string or :array data types."
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
# Here is the where the 'magic' begins. For each property defined, we will
|
|
402
|
+
# generate special setters and getters that will take advantage of ActiveModel
|
|
403
|
+
# helpers.
|
|
404
|
+
# get the default value if provided (or Proc)
|
|
405
|
+
default_value = opts[:default]
|
|
406
|
+
unless default_value.nil?
|
|
407
|
+
defaults_list.push(key) unless default_value.nil?
|
|
408
|
+
|
|
409
|
+
define_method("#{key}_default") do
|
|
410
|
+
# If the default object provided is a Proc, then run the proc, otherwise
|
|
411
|
+
# we'll assume it's just a plain literal value
|
|
412
|
+
default_value.is_a?(Proc) ? default_value.call(self) : default_value
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
# We define a getter with the key
|
|
417
|
+
|
|
418
|
+
define_method(key) do
|
|
419
|
+
|
|
420
|
+
# we will get the value using the internal value of the instance variable
|
|
421
|
+
# using the instance_variable_get
|
|
422
|
+
value = instance_variable_get ivar
|
|
423
|
+
|
|
424
|
+
# If the value is nil and this current Parse::Object instance is a pointer?
|
|
425
|
+
# then someone is calling the getter for this, which means they probably want
|
|
426
|
+
# its value - so let's go turn this pointer into a full object record.
|
|
427
|
+
# Also autofetch if object was selectively fetched and this field wasn't included.
|
|
428
|
+
should_autofetch = value.nil? && (pointer? || (has_selective_keys? && !field_was_fetched?(key)))
|
|
429
|
+
if should_autofetch
|
|
430
|
+
# If autofetch is disabled and we're accessing an unfetched field on a
|
|
431
|
+
# selectively fetched object, raise an error to make the issue explicit
|
|
432
|
+
if autofetch_disabled? && has_selective_keys? && !field_was_fetched?(key)
|
|
433
|
+
raise Parse::UnfetchedFieldAccessError.new(key, self.class.name)
|
|
434
|
+
end
|
|
435
|
+
# call autofetch to fetch the entire record
|
|
436
|
+
# and then get the ivar again cause it might have been updated.
|
|
437
|
+
autofetch!(key)
|
|
438
|
+
value = instance_variable_get ivar
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
# if value is nil (even after fetching), then lets see if the developer
|
|
442
|
+
# set a default value for this attribute.
|
|
443
|
+
if value.nil? && respond_to?("#{key}_default")
|
|
444
|
+
value = send("#{key}_default")
|
|
445
|
+
value = format_value(key, value, data_type)
|
|
446
|
+
# lets set the variable with the updated value
|
|
447
|
+
instance_variable_set ivar, value
|
|
448
|
+
send will_change_method
|
|
449
|
+
elsif value.nil? && data_type == :array
|
|
450
|
+
value = Parse::CollectionProxy.new [], delegate: self, key: key
|
|
451
|
+
instance_variable_set ivar, value
|
|
452
|
+
# don't send the notification yet until they actually add something
|
|
453
|
+
# which will be handled by the collection proxy.
|
|
454
|
+
# send will_change_method
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
# if the value is a String (like an iso8601 date) and the data type of
|
|
458
|
+
# this object is :date, then let's be nice and create a parse date for it.
|
|
459
|
+
if value.is_a?(String) && data_type == :date
|
|
460
|
+
value = format_value(key, value, data_type)
|
|
461
|
+
instance_variable_set ivar, value
|
|
462
|
+
send will_change_method
|
|
463
|
+
end
|
|
464
|
+
# finally return the value
|
|
465
|
+
if symbolize_value
|
|
466
|
+
if data_type == :string
|
|
467
|
+
return value.respond_to?(:to_sym) ? value.to_sym : value
|
|
468
|
+
elsif data_type == :array && value.is_a?(Array)
|
|
469
|
+
# value.map(&:to_sym)
|
|
470
|
+
return value.compact.map { |m| m.respond_to?(:to_sym) ? m.to_sym : m }
|
|
471
|
+
end
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
value
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
# support question mark methods for boolean
|
|
478
|
+
if data_type == :boolean
|
|
479
|
+
if self.method_defined?("#{key}?")
|
|
480
|
+
warn "Creating boolean helper :#{key}?. Will overwrite existing method #{self}##{key}?."
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
# returns true if set to true, false otherwise
|
|
484
|
+
define_method("#{key}?") { (send(key) == true) }
|
|
485
|
+
unless opts[:scopes] == false
|
|
486
|
+
scope key, ->(opts = {}) { query(opts.merge(key => true)) }
|
|
487
|
+
end
|
|
488
|
+
elsif data_type == :integer || data_type == :float
|
|
489
|
+
if self.method_defined?("#{key}_increment!")
|
|
490
|
+
warn "Creating increment helper :#{key}_increment!. Will overwrite existing method #{self}##{key}_increment!."
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
define_method("#{key}_increment!") do |amount = 1|
|
|
494
|
+
unless amount.is_a?(Numeric)
|
|
495
|
+
raise ArgumentError, "Amount needs to be an integer"
|
|
496
|
+
end
|
|
497
|
+
result = self.op_increment!(key, amount)
|
|
498
|
+
if result
|
|
499
|
+
new_value = send(key).to_i + amount
|
|
500
|
+
# set the updated value, with no dirty tracking
|
|
501
|
+
self.send set_attribute_method, new_value, false
|
|
502
|
+
end
|
|
503
|
+
result
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
if self.method_defined?("#{key}_decrement!")
|
|
507
|
+
warn "Creating decrement helper :#{key}_decrement!. Will overwrite existing method #{self}##{key}_decrement!."
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
define_method("#{key}_decrement!") do |amount = -1|
|
|
511
|
+
unless amount.is_a?(Numeric)
|
|
512
|
+
raise ArgumentError, "Amount needs to be an integer"
|
|
513
|
+
end
|
|
514
|
+
amount = -amount if amount > 0
|
|
515
|
+
send("#{key}_increment!", amount)
|
|
516
|
+
end
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
# The second method to be defined is a setter method. This is done by
|
|
520
|
+
# defining :key with a '=' sign. However, to support setting the attribute
|
|
521
|
+
# with and without dirty tracking, we really will just proxy it to another method
|
|
522
|
+
|
|
523
|
+
define_method("#{key}=") do |val|
|
|
524
|
+
#we proxy the method passing the value and true. Passing true to the
|
|
525
|
+
# method tells it to make sure dirty tracking is enabled.
|
|
526
|
+
self.send set_attribute_method, val, true
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
# This is the real setter method. Takes two arguments, the value to set
|
|
530
|
+
# and whether to mark it as dirty tracked.
|
|
531
|
+
define_method(set_attribute_method) do |val, track = true|
|
|
532
|
+
# Each value has a data type, based on that we can treat the incoming
|
|
533
|
+
# value as input, and format it to the correct storage format. This method is
|
|
534
|
+
# defined in this file (instance method)
|
|
535
|
+
val = format_value(key, val, data_type)
|
|
536
|
+
# if dirty trackin is enabled, call the ActiveModel required method of _will_change!
|
|
537
|
+
# this will grab the current value and keep a copy of it - but we only do this if
|
|
538
|
+
# the new value being set is different from the current value stored.
|
|
539
|
+
if track == true
|
|
540
|
+
prepare_for_dirty_tracking!(key)
|
|
541
|
+
send will_change_method unless val == instance_variable_get(ivar)
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
if symbolize_value
|
|
545
|
+
if data_type == :string
|
|
546
|
+
val = nil if val.blank?
|
|
547
|
+
val = val.to_sym if val.respond_to?(:to_sym)
|
|
548
|
+
elsif val.is_a?(Parse::CollectionProxy)
|
|
549
|
+
items = val.collection.map { |m| m.respond_to?(:to_sym) ? m.to_sym : m }
|
|
550
|
+
val.set_collection! items
|
|
551
|
+
end
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
# if is_enum_type
|
|
555
|
+
#
|
|
556
|
+
# end
|
|
557
|
+
# now set the instance value
|
|
558
|
+
instance_variable_set ivar, val
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
# The core methods above support all attributes with the base local :key parameter
|
|
562
|
+
# however, for ease of use and to handle that the incoming fields from parse have different
|
|
563
|
+
# names, we will alias all those methods defined above with the defined parse_field.
|
|
564
|
+
# if both the local name matches the calculated/provided remote column name, don't create
|
|
565
|
+
# an alias method since it is the same thing. Ex. attribute 'username' would probably have the
|
|
566
|
+
# remote column name also called 'username'.
|
|
567
|
+
return true if parse_field == key
|
|
568
|
+
|
|
569
|
+
# we will now create the aliases, however if the method is already defined
|
|
570
|
+
# we warn the user unless the field is :objectId since we are in charge of that one.
|
|
571
|
+
# this is because it is possible they want to override. You can turn off this
|
|
572
|
+
# behavior by passing false to :alias
|
|
573
|
+
|
|
574
|
+
if self.method_defined?(parse_field) == false && opts[:alias]
|
|
575
|
+
alias_method parse_field, key
|
|
576
|
+
alias_method "#{parse_field}=", "#{key}="
|
|
577
|
+
alias_method "#{parse_field}_set_attribute!", set_attribute_method
|
|
578
|
+
elsif parse_field.to_sym != :objectId
|
|
579
|
+
warn "Alias property method #{self}##{parse_field} already defined."
|
|
580
|
+
end
|
|
581
|
+
true
|
|
582
|
+
end # property
|
|
583
|
+
end #ClassMethods
|
|
584
|
+
|
|
585
|
+
# @return [Hash] a hash mapping of all property fields and their types.
|
|
586
|
+
def field_map
|
|
587
|
+
self.class.field_map
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
# @return returns the list of fields
|
|
591
|
+
def fields(type = nil)
|
|
592
|
+
self.class.fields(type)
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
# TODO: We can optimize
|
|
596
|
+
# @return [Hash] returns the list of property attributes for this class.
|
|
597
|
+
def attributes
|
|
598
|
+
{ __type: :string, :className => :string }.merge!(self.class.attributes)
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
# support for setting a hash of attributes on the object with a given dirty tracking value
|
|
602
|
+
# if dirty_track: is set to false (default), attributes are set without dirty tracking.
|
|
603
|
+
# Allos mass assignment of properties with a provided hash.
|
|
604
|
+
# @param hash [Hash] the hash matching the property field names.
|
|
605
|
+
# @param dirty_track [Boolean] whether dirty tracking be enabled. When true,
|
|
606
|
+
# permission-sensitive keys ({Parse::Properties::PROTECTED_MASS_ASSIGNMENT_KEYS})
|
|
607
|
+
# are skipped by default so attacker-controlled params cannot overwrite
|
|
608
|
+
# acl/roles/sessionToken/etc. Set explicitly via the typed property
|
|
609
|
+
# writers when the caller is trusted.
|
|
610
|
+
# @param filter_protected [Boolean, nil] whether to filter out
|
|
611
|
+
# {Parse::Properties::PROTECTED_MASS_ASSIGNMENT_KEYS}. Defaults to
|
|
612
|
+
# +dirty_track+ for backwards-compat (the historical coupling). Callers
|
|
613
|
+
# can pass +true+ explicitly to filter even on the trusted hydration
|
|
614
|
+
# path (used by {Parse::Object#initialize} when constructed with
|
|
615
|
+
# +trusted: false+ but an +objectId+ is in the hash). +false+ explicitly
|
|
616
|
+
# preserves the legacy "server response" semantics.
|
|
617
|
+
# @param protected_set [Array<String>, nil] override which key list to
|
|
618
|
+
# filter when +filter_protected+ is true. Defaults to the wider
|
|
619
|
+
# {Parse::Properties::PROTECTED_MASS_ASSIGNMENT_KEYS}.
|
|
620
|
+
# {Parse::Object#initialize} passes
|
|
621
|
+
# {Parse::Properties::PROTECTED_INITIALIZE_KEYS} here to allow
|
|
622
|
+
# legitimate hydration patterns (`Klass.new("objectId" => …,
|
|
623
|
+
# "createdAt" => …)`) while still refusing security-critical
|
|
624
|
+
# forgeries (`sessionToken`, `_rperm`, `authData`, …).
|
|
625
|
+
# @return [Hash]
|
|
626
|
+
def apply_attributes!(hash, dirty_track: false, filter_protected: nil, protected_set: nil)
|
|
627
|
+
return unless hash.is_a?(Hash)
|
|
628
|
+
|
|
629
|
+
filter_protected = dirty_track if filter_protected.nil?
|
|
630
|
+
protected_set ||= Parse::Properties::PROTECTED_MASS_ASSIGNMENT_KEYS
|
|
631
|
+
protected_keys = filter_protected ? protected_set : nil
|
|
632
|
+
# Internal hydration path lifts objectId out of the response hash. The
|
|
633
|
+
# mass-assignment path must not, or attacker-controlled params can
|
|
634
|
+
# overwrite the primary key of an in-memory object.
|
|
635
|
+
unless dirty_track
|
|
636
|
+
@id ||= hash[Parse::Model::ID] || hash[Parse::Model::OBJECT_ID] || hash[:objectId]
|
|
637
|
+
end
|
|
638
|
+
hash.each do |key, value|
|
|
639
|
+
next if protected_keys && protected_keys.include?(key.to_s)
|
|
640
|
+
method = "#{key}_set_attribute!".freeze
|
|
641
|
+
send(method, value, dirty_track) if respond_to?(method)
|
|
642
|
+
end
|
|
643
|
+
end
|
|
644
|
+
|
|
645
|
+
# Supports mass assignment of attributes
|
|
646
|
+
# @return (see #apply_attributes!)
|
|
647
|
+
def attributes=(hash)
|
|
648
|
+
return unless hash.is_a?(Hash)
|
|
649
|
+
# - [:id, :objectId]
|
|
650
|
+
# only overwrite @id if it hasn't been set.
|
|
651
|
+
apply_attributes!(hash, dirty_track: true)
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
# Returns a hash of attributes for properties that have changed. This will
|
|
655
|
+
# not include any of the base attributes (ex. id, created_at, etc).
|
|
656
|
+
# This method helps generate the change payload that will be sent when saving
|
|
657
|
+
# objects to Parse.
|
|
658
|
+
# @param include_all [Boolean] whether to include all {Parse::Properties::BASE_KEYS} attributes.
|
|
659
|
+
# @return [Hash]
|
|
660
|
+
def attribute_updates(include_all = false)
|
|
661
|
+
# TODO: Replace this algorithm with reduce()
|
|
662
|
+
h = {}
|
|
663
|
+
changed.each do |key|
|
|
664
|
+
key = key.to_sym
|
|
665
|
+
next if include_all == false && Parse::Properties::BASE_KEYS.include?(key)
|
|
666
|
+
next unless fields[key].present?
|
|
667
|
+
remote_field = self.field_map[key] || key
|
|
668
|
+
h[remote_field] = send key
|
|
669
|
+
h[remote_field] = { __op: :Delete } if h[remote_field].nil?
|
|
670
|
+
# in the case that the field is a Parse object, generate a pointer
|
|
671
|
+
# if it is a Parse::PointerCollectionProxy, then make sure we get a list of pointers.
|
|
672
|
+
h[remote_field] = h[remote_field].parse_pointers if h[remote_field].is_a?(Parse::PointerCollectionProxy)
|
|
673
|
+
# For regular CollectionProxy arrays containing Parse objects, convert to pointers for storage
|
|
674
|
+
if h[remote_field].is_a?(Parse::CollectionProxy) && !h[remote_field].is_a?(Parse::PointerCollectionProxy)
|
|
675
|
+
h[remote_field] = h[remote_field].as_json(pointers_only: true)
|
|
676
|
+
end
|
|
677
|
+
h[remote_field] = h[remote_field].pointer if h[remote_field].respond_to?(:pointer)
|
|
678
|
+
end
|
|
679
|
+
h
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
# @return [Boolean] true if any of the attributes have changed.
|
|
683
|
+
def attribute_changes?
|
|
684
|
+
changed.any? do |key|
|
|
685
|
+
fields[key.to_sym].present?
|
|
686
|
+
end
|
|
687
|
+
end
|
|
688
|
+
|
|
689
|
+
# Returns a formatted value based on the operation hash and data_type of the property.
|
|
690
|
+
# For some values in Parse, they are specified as operation hashes which could include
|
|
691
|
+
# Add, Remove, Delete, AddUnique and Increment.
|
|
692
|
+
# @param key [Symbol] the name of the property
|
|
693
|
+
# @param val [Hash] the Parse operation hash value.
|
|
694
|
+
# @param data_type [Symbol] The data type of the property.
|
|
695
|
+
# @return [Object]
|
|
696
|
+
def format_operation(key, val, data_type)
|
|
697
|
+
return val unless val.is_a?(Hash) && val["__op"].present?
|
|
698
|
+
op = val["__op"]
|
|
699
|
+
ivar = :"@#{key}"
|
|
700
|
+
#handles delete case otherwise 'null' shows up in column
|
|
701
|
+
if "Delete" == op
|
|
702
|
+
val = nil
|
|
703
|
+
elsif "Add" == op && data_type == :array
|
|
704
|
+
val = (instance_variable_get(ivar) || []).to_a + (val["objects"] || [])
|
|
705
|
+
elsif "Remove" == op && data_type == :array
|
|
706
|
+
val = (instance_variable_get(ivar) || []).to_a - (val["objects"] || [])
|
|
707
|
+
elsif "AddUnique" == op && data_type == :array
|
|
708
|
+
objects = (val["objects"] || []).uniq
|
|
709
|
+
original_items = (instance_variable_get(ivar) || []).to_a
|
|
710
|
+
objects.reject! { |r| original_items.include?(r) }
|
|
711
|
+
val = original_items + objects
|
|
712
|
+
elsif "Increment" == op && data_type == :integer || data_type == :integer
|
|
713
|
+
# for operations that increment by a certain amount, they come as a hash
|
|
714
|
+
val = (instance_variable_get(ivar) || 0) + (val["amount"] || 0).to_i
|
|
715
|
+
end
|
|
716
|
+
val
|
|
717
|
+
end
|
|
718
|
+
|
|
719
|
+
# this method takes an input value and transforms it to the proper local format
|
|
720
|
+
# depending on the data type that was set for a particular property key.
|
|
721
|
+
# Return the internal representation of a property value for a given data type.
|
|
722
|
+
# @param key [Symbol] the name of the property
|
|
723
|
+
# @param val [Object] the value to format.
|
|
724
|
+
# @param data_type [Symbol] provide a hint to the data_type of this value.
|
|
725
|
+
# @return [Object]
|
|
726
|
+
def format_value(key, val, data_type = nil)
|
|
727
|
+
# if data_type wasn't passed, then get the data_type from the fields hash
|
|
728
|
+
data_type ||= self.fields[key]
|
|
729
|
+
|
|
730
|
+
val = format_operation(key, val, data_type)
|
|
731
|
+
|
|
732
|
+
case data_type
|
|
733
|
+
when :object
|
|
734
|
+
val = val.with_indifferent_access if val.is_a?(Hash)
|
|
735
|
+
when :array
|
|
736
|
+
# All "array" types use a collection proxy
|
|
737
|
+
val = val.to_a if val.is_a?(Parse::CollectionProxy) #all objects must be in array form
|
|
738
|
+
val = [val] unless val.is_a?(Array) #all objects must be in array form
|
|
739
|
+
val.compact! #remove any nil
|
|
740
|
+
val = Parse::CollectionProxy.new val, delegate: self, key: key
|
|
741
|
+
when :geopoint
|
|
742
|
+
val = Parse::GeoPoint.new(val) unless val.blank?
|
|
743
|
+
when :polygon
|
|
744
|
+
val = Parse::Polygon.new(val) unless val.blank?
|
|
745
|
+
when :file
|
|
746
|
+
if val.is_a?(Hash) && val["__type"] == "File"
|
|
747
|
+
val = Parse::File.new(val)
|
|
748
|
+
elsif !val.blank?
|
|
749
|
+
val = Parse::File.new(val)
|
|
750
|
+
end
|
|
751
|
+
when :bytes
|
|
752
|
+
if val.is_a?(Hash) && val["__type"] == "Bytes"
|
|
753
|
+
val = Parse::Bytes.new(val["base64"] || val[:base64])
|
|
754
|
+
elsif !val.blank?
|
|
755
|
+
val = Parse::Bytes.new(val)
|
|
756
|
+
end
|
|
757
|
+
when :integer
|
|
758
|
+
if val.nil? || val.respond_to?(:to_i) == false
|
|
759
|
+
val = nil
|
|
760
|
+
else
|
|
761
|
+
val = val.to_i
|
|
762
|
+
end
|
|
763
|
+
when :boolean
|
|
764
|
+
if val.nil?
|
|
765
|
+
val = nil
|
|
766
|
+
else
|
|
767
|
+
val = val ? true : false
|
|
768
|
+
end
|
|
769
|
+
when :string
|
|
770
|
+
val = val.to_s unless val.blank?
|
|
771
|
+
when :float
|
|
772
|
+
val = val.to_f unless val.blank?
|
|
773
|
+
when :acl
|
|
774
|
+
# ACL types go through a special conversion
|
|
775
|
+
val = ACL.typecast(val, self)
|
|
776
|
+
when :date
|
|
777
|
+
# if it respond to parse_date, then use that as the conversion.
|
|
778
|
+
if val.respond_to?(:parse_date) && val.is_a?(Parse::Date) == false
|
|
779
|
+
val = val.parse_date
|
|
780
|
+
# if the value is a hash, then it may be the Parse hash format for an iso date.
|
|
781
|
+
elsif val.is_a?(Hash) # val.respond_to?(:iso8601)
|
|
782
|
+
iso_val = (val["iso"] || val[:iso]).to_s.strip.presence
|
|
783
|
+
val = iso_val ? Parse::Date.parse(iso_val) : nil
|
|
784
|
+
elsif val.is_a?(String)
|
|
785
|
+
# if it's a string, try parsing the date
|
|
786
|
+
val = (stripped = val.strip).present? ? Parse::Date.parse(stripped) : nil
|
|
787
|
+
#elsif val.present?
|
|
788
|
+
# pus "[Parse::Stack] Invalid date value '#{val}' assigned to #{self.class}##{key}, it should be a Parse::Date or DateTime."
|
|
789
|
+
# raise ValueError, "Invalid date value '#{val}' assigned to #{self.class}##{key}, it should be a Parse::Date or DateTime."
|
|
790
|
+
end
|
|
791
|
+
when :timezone
|
|
792
|
+
val = Parse::TimeZone.new(val) if val.present?
|
|
793
|
+
when :phone
|
|
794
|
+
val = Parse::Phone.new(val) if val.present?
|
|
795
|
+
when :email
|
|
796
|
+
val = Parse::Email.new(val) if val.present?
|
|
797
|
+
else
|
|
798
|
+
# You can provide a specific class instead of a symbol format
|
|
799
|
+
if data_type.respond_to?(:typecast)
|
|
800
|
+
val = data_type.typecast(val)
|
|
801
|
+
else
|
|
802
|
+
warn "Property :#{key}: :#{data_type} has no valid data type"
|
|
803
|
+
val = val #default
|
|
804
|
+
end
|
|
805
|
+
end
|
|
806
|
+
val
|
|
807
|
+
end
|
|
808
|
+
end # Properties
|
|
809
|
+
end # Parse
|