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.
Files changed (178) hide show
  1. checksums.yaml +7 -0
  2. data/.bundle/config +2 -0
  3. data/.env.sample +112 -0
  4. data/.env.test +10 -0
  5. data/.github/workflows/ruby.yml +36 -0
  6. data/.gitignore +49 -0
  7. data/.ruby-version +1 -0
  8. data/.solargraph.yml +22 -0
  9. data/CHANGELOG.md +5816 -0
  10. data/Gemfile +30 -0
  11. data/Gemfile.lock +175 -0
  12. data/LICENSE.txt +23 -0
  13. data/Makefile +63 -0
  14. data/README.md +5655 -0
  15. data/Rakefile +573 -0
  16. data/bin/console +38 -0
  17. data/bin/parse-console +136 -0
  18. data/bin/server +17 -0
  19. data/bin/setup +7 -0
  20. data/config/parse-config.json +12 -0
  21. data/docs/TEST_SERVER.md +271 -0
  22. data/docs/_config.yml +1 -0
  23. data/docs/mcp_guide.md +3484 -0
  24. data/docs/mongodb_direct_guide.md +1348 -0
  25. data/docs/mongodb_index_optimization_guide.md +631 -0
  26. data/examples/transaction_example.rb +219 -0
  27. data/lib/parse/acl_scope.rb +728 -0
  28. data/lib/parse/agent/cancellation_token.rb +80 -0
  29. data/lib/parse/agent/constraint_translator.rb +480 -0
  30. data/lib/parse/agent/describe.rb +420 -0
  31. data/lib/parse/agent/errors.rb +133 -0
  32. data/lib/parse/agent/mcp_client.rb +557 -0
  33. data/lib/parse/agent/mcp_dispatcher.rb +1023 -0
  34. data/lib/parse/agent/mcp_rack_app.rb +1143 -0
  35. data/lib/parse/agent/mcp_server.rb +376 -0
  36. data/lib/parse/agent/metadata_audit.rb +259 -0
  37. data/lib/parse/agent/metadata_dsl.rb +733 -0
  38. data/lib/parse/agent/metadata_registry.rb +794 -0
  39. data/lib/parse/agent/pipeline_validator.rb +82 -0
  40. data/lib/parse/agent/prompts.rb +351 -0
  41. data/lib/parse/agent/rate_limiter.rb +158 -0
  42. data/lib/parse/agent/relation_graph.rb +162 -0
  43. data/lib/parse/agent/result_formatter.rb +453 -0
  44. data/lib/parse/agent/tools.rb +5489 -0
  45. data/lib/parse/agent.rb +3249 -0
  46. data/lib/parse/api/aggregate.rb +79 -0
  47. data/lib/parse/api/all.rb +26 -0
  48. data/lib/parse/api/analytics.rb +18 -0
  49. data/lib/parse/api/batch.rb +33 -0
  50. data/lib/parse/api/cloud_functions.rb +58 -0
  51. data/lib/parse/api/config.rb +125 -0
  52. data/lib/parse/api/files.rb +29 -0
  53. data/lib/parse/api/hooks.rb +117 -0
  54. data/lib/parse/api/objects.rb +146 -0
  55. data/lib/parse/api/path_segment.rb +75 -0
  56. data/lib/parse/api/push.rb +20 -0
  57. data/lib/parse/api/schema.rb +49 -0
  58. data/lib/parse/api/server.rb +50 -0
  59. data/lib/parse/api/sessions.rb +24 -0
  60. data/lib/parse/api/users.rb +250 -0
  61. data/lib/parse/atlas_search/index_manager.rb +353 -0
  62. data/lib/parse/atlas_search/result.rb +204 -0
  63. data/lib/parse/atlas_search/search_builder.rb +604 -0
  64. data/lib/parse/atlas_search/session.rb +253 -0
  65. data/lib/parse/atlas_search.rb +995 -0
  66. data/lib/parse/client/authentication.rb +97 -0
  67. data/lib/parse/client/batch.rb +234 -0
  68. data/lib/parse/client/body_builder.rb +240 -0
  69. data/lib/parse/client/caching.rb +203 -0
  70. data/lib/parse/client/logging.rb +293 -0
  71. data/lib/parse/client/profiling.rb +181 -0
  72. data/lib/parse/client/protocol.rb +91 -0
  73. data/lib/parse/client/request.rb +233 -0
  74. data/lib/parse/client/response.rb +208 -0
  75. data/lib/parse/client.rb +1104 -0
  76. data/lib/parse/clp_scope.rb +361 -0
  77. data/lib/parse/live_query/circuit_breaker.rb +256 -0
  78. data/lib/parse/live_query/client.rb +1001 -0
  79. data/lib/parse/live_query/configuration.rb +224 -0
  80. data/lib/parse/live_query/event.rb +115 -0
  81. data/lib/parse/live_query/event_queue.rb +272 -0
  82. data/lib/parse/live_query/health_monitor.rb +214 -0
  83. data/lib/parse/live_query/logging.rb +149 -0
  84. data/lib/parse/live_query/subscription.rb +294 -0
  85. data/lib/parse/live_query.rb +163 -0
  86. data/lib/parse/lookup_rewriter.rb +445 -0
  87. data/lib/parse/model/acl.rb +968 -0
  88. data/lib/parse/model/associations/belongs_to.rb +275 -0
  89. data/lib/parse/model/associations/collection_proxy.rb +435 -0
  90. data/lib/parse/model/associations/has_many.rb +597 -0
  91. data/lib/parse/model/associations/has_one.rb +158 -0
  92. data/lib/parse/model/associations/pointer_collection_proxy.rb +134 -0
  93. data/lib/parse/model/associations/relation_collection_proxy.rb +177 -0
  94. data/lib/parse/model/bytes.rb +62 -0
  95. data/lib/parse/model/classes/audience.rb +262 -0
  96. data/lib/parse/model/classes/installation.rb +363 -0
  97. data/lib/parse/model/classes/job_schedule.rb +153 -0
  98. data/lib/parse/model/classes/job_status.rb +264 -0
  99. data/lib/parse/model/classes/product.rb +75 -0
  100. data/lib/parse/model/classes/push_status.rb +263 -0
  101. data/lib/parse/model/classes/role.rb +751 -0
  102. data/lib/parse/model/classes/session.rb +201 -0
  103. data/lib/parse/model/classes/user.rb +943 -0
  104. data/lib/parse/model/clp.rb +544 -0
  105. data/lib/parse/model/core/actions.rb +1268 -0
  106. data/lib/parse/model/core/builder.rb +139 -0
  107. data/lib/parse/model/core/create_lock.rb +386 -0
  108. data/lib/parse/model/core/describe.rb +382 -0
  109. data/lib/parse/model/core/enhanced_change_tracking.rb +159 -0
  110. data/lib/parse/model/core/errors.rb +38 -0
  111. data/lib/parse/model/core/fetching.rb +566 -0
  112. data/lib/parse/model/core/field_guards.rb +220 -0
  113. data/lib/parse/model/core/indexing.rb +382 -0
  114. data/lib/parse/model/core/parse_reference.rb +407 -0
  115. data/lib/parse/model/core/properties.rb +809 -0
  116. data/lib/parse/model/core/querying.rb +491 -0
  117. data/lib/parse/model/core/schema.rb +202 -0
  118. data/lib/parse/model/core/search_indexing.rb +174 -0
  119. data/lib/parse/model/date.rb +88 -0
  120. data/lib/parse/model/email.rb +213 -0
  121. data/lib/parse/model/file.rb +527 -0
  122. data/lib/parse/model/geojson.rb +271 -0
  123. data/lib/parse/model/geopoint.rb +261 -0
  124. data/lib/parse/model/model.rb +260 -0
  125. data/lib/parse/model/object.rb +2068 -0
  126. data/lib/parse/model/phone.rb +520 -0
  127. data/lib/parse/model/pointer.rb +443 -0
  128. data/lib/parse/model/polygon.rb +406 -0
  129. data/lib/parse/model/push.rb +975 -0
  130. data/lib/parse/model/shortnames.rb +8 -0
  131. data/lib/parse/model/time_zone.rb +141 -0
  132. data/lib/parse/model/validations/uniqueness_validator.rb +97 -0
  133. data/lib/parse/model/validations.rb +96 -0
  134. data/lib/parse/mongodb.rb +2300 -0
  135. data/lib/parse/pipeline_security.rb +554 -0
  136. data/lib/parse/query/constraint.rb +198 -0
  137. data/lib/parse/query/constraints.rb +3279 -0
  138. data/lib/parse/query/cursor.rb +434 -0
  139. data/lib/parse/query/n_plus_one_detector.rb +445 -0
  140. data/lib/parse/query/operation.rb +104 -0
  141. data/lib/parse/query/ordering.rb +66 -0
  142. data/lib/parse/query.rb +7028 -0
  143. data/lib/parse/schema/index_migrator.rb +291 -0
  144. data/lib/parse/schema/search_index_migrator.rb +289 -0
  145. data/lib/parse/schema.rb +494 -0
  146. data/lib/parse/stack/generators/rails.rb +40 -0
  147. data/lib/parse/stack/generators/templates/model.erb +51 -0
  148. data/lib/parse/stack/generators/templates/model_installation.rb +4 -0
  149. data/lib/parse/stack/generators/templates/model_role.rb +4 -0
  150. data/lib/parse/stack/generators/templates/model_session.rb +4 -0
  151. data/lib/parse/stack/generators/templates/model_user.rb +11 -0
  152. data/lib/parse/stack/generators/templates/parse.rb +12 -0
  153. data/lib/parse/stack/generators/templates/webhooks.rb +10 -0
  154. data/lib/parse/stack/railtie.rb +18 -0
  155. data/lib/parse/stack/tasks.rb +563 -0
  156. data/lib/parse/stack/version.rb +11 -0
  157. data/lib/parse/stack.rb +455 -0
  158. data/lib/parse/two_factor_auth/user_extension.rb +449 -0
  159. data/lib/parse/two_factor_auth.rb +310 -0
  160. data/lib/parse/webhooks/payload.rb +360 -0
  161. data/lib/parse/webhooks/registration.rb +199 -0
  162. data/lib/parse/webhooks/replay_protection.rb +189 -0
  163. data/lib/parse/webhooks.rb +510 -0
  164. data/lib/parse-stack-next.rb +5 -0
  165. data/lib/parse-stack.rb +5 -0
  166. data/parse-stack-next.gemspec +82 -0
  167. data/parse-stack.png +0 -0
  168. data/scripts/debug-ips.js +35 -0
  169. data/scripts/docker/Dockerfile.parse +13 -0
  170. data/scripts/docker/atlas-init.js +284 -0
  171. data/scripts/docker/docker-compose.atlas.yml +76 -0
  172. data/scripts/docker/docker-compose.test.yml +106 -0
  173. data/scripts/docker/mongo-init.js +21 -0
  174. data/scripts/eval_mcp_with_lm_studio.rb +274 -0
  175. data/scripts/start-parse.sh +90 -0
  176. data/scripts/start_mcp_server.rb +78 -0
  177. data/scripts/test_server_connection.rb +82 -0
  178. 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