parse-stack-next 4.5.0 → 5.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. checksums.yaml +4 -4
  2. data/.bundle/config +2 -0
  3. data/.env.sample +17 -3
  4. data/.github/workflows/codeql.yml +44 -0
  5. data/.github/workflows/docs.yml +39 -0
  6. data/.github/workflows/release.yml +32 -0
  7. data/.github/workflows/ruby.yml +8 -6
  8. data/.gitignore +4 -0
  9. data/.vscode/settings.json +3 -0
  10. data/CHANGELOG.md +305 -72
  11. data/Gemfile.lock +10 -3
  12. data/LICENSE.txt +1 -1
  13. data/README.md +190 -219
  14. data/Rakefile +1 -1
  15. data/SECURITY.md +30 -0
  16. data/assets/parse-stack-next-avatar.png +0 -0
  17. data/assets/parse-stack-next-avatar.svg +37 -0
  18. data/assets/parse-stack-next-banner.png +0 -0
  19. data/assets/parse-stack-next-banner.svg +45 -0
  20. data/assets/parse-stack-next-social-preview.png +0 -0
  21. data/docs/atlas_vector_search_guide.md +511 -0
  22. data/docs/client_sdk_guide.md +1320 -0
  23. data/docs/mcp_guide.md +225 -104
  24. data/docs/mongodb_direct_guide.md +21 -4
  25. data/docs/usage_guide.md +585 -0
  26. data/examples/transaction_example.rb +28 -28
  27. data/lib/parse/acl_scope.rb +2 -2
  28. data/lib/parse/agent/mcp_rack_app.rb +184 -16
  29. data/lib/parse/agent/metadata_dsl.rb +16 -16
  30. data/lib/parse/agent/pipeline_validator.rb +28 -1
  31. data/lib/parse/agent/prompts.rb +5 -5
  32. data/lib/parse/agent/tools.rb +287 -14
  33. data/lib/parse/agent.rb +209 -12
  34. data/lib/parse/api/analytics.rb +27 -5
  35. data/lib/parse/api/files.rb +6 -2
  36. data/lib/parse/api/push.rb +21 -4
  37. data/lib/parse/api/server.rb +59 -0
  38. data/lib/parse/api/users.rb +26 -2
  39. data/lib/parse/atlas_search/index_manager.rb +84 -0
  40. data/lib/parse/atlas_search.rb +37 -9
  41. data/lib/parse/cache/pool.rb +88 -0
  42. data/lib/parse/cache/redis.rb +249 -0
  43. data/lib/parse/client/body_builder.rb +94 -0
  44. data/lib/parse/client/caching.rb +109 -9
  45. data/lib/parse/client/response.rb +27 -0
  46. data/lib/parse/client.rb +74 -3
  47. data/lib/parse/console.rb +203 -0
  48. data/lib/parse/embeddings/cohere.rb +484 -0
  49. data/lib/parse/embeddings/fixture.rb +130 -0
  50. data/lib/parse/embeddings/jina.rb +454 -0
  51. data/lib/parse/embeddings/local_http.rb +492 -0
  52. data/lib/parse/embeddings/openai.rb +520 -0
  53. data/lib/parse/embeddings/provider.rb +264 -0
  54. data/lib/parse/embeddings/qwen.rb +431 -0
  55. data/lib/parse/embeddings/voyage.rb +550 -0
  56. data/lib/parse/embeddings.rb +225 -0
  57. data/lib/parse/graphql/scalars.rb +53 -0
  58. data/lib/parse/graphql/type_generator.rb +264 -0
  59. data/lib/parse/graphql.rb +48 -0
  60. data/lib/parse/live_query/client.rb +24 -5
  61. data/lib/parse/live_query/subscription.rb +17 -6
  62. data/lib/parse/live_query.rb +9 -4
  63. data/lib/parse/model/associations/collection_proxy.rb +2 -2
  64. data/lib/parse/model/associations/has_many.rb +32 -1
  65. data/lib/parse/model/associations/has_one.rb +17 -0
  66. data/lib/parse/model/associations/pointer_collection_proxy.rb +3 -3
  67. data/lib/parse/model/classes/user.rb +307 -11
  68. data/lib/parse/model/clp.rb +1 -1
  69. data/lib/parse/model/core/create_lock.rb +14 -2
  70. data/lib/parse/model/core/embed_managed.rb +296 -0
  71. data/lib/parse/model/core/fetching.rb +4 -4
  72. data/lib/parse/model/core/indexing.rb +53 -14
  73. data/lib/parse/model/core/parse_reference.rb +3 -3
  74. data/lib/parse/model/core/properties.rb +70 -1
  75. data/lib/parse/model/core/querying.rb +57 -1
  76. data/lib/parse/model/core/vector_searchable.rb +285 -0
  77. data/lib/parse/model/file.rb +16 -4
  78. data/lib/parse/model/model.rb +26 -10
  79. data/lib/parse/model/object.rb +63 -6
  80. data/lib/parse/model/pointer.rb +16 -2
  81. data/lib/parse/model/shortnames.rb +2 -0
  82. data/lib/parse/model/validations/uniqueness_validator.rb +3 -3
  83. data/lib/parse/model/vector.rb +102 -0
  84. data/lib/parse/mongodb.rb +90 -8
  85. data/lib/parse/pipeline_security.rb +59 -2
  86. data/lib/parse/query/constraints.rb +16 -14
  87. data/lib/parse/query/ordering.rb +1 -1
  88. data/lib/parse/query.rb +137 -64
  89. data/lib/parse/stack/generators/templates/model.erb +2 -2
  90. data/lib/parse/stack/generators/templates/model_installation.rb +1 -1
  91. data/lib/parse/stack/generators/templates/model_role.rb +1 -1
  92. data/lib/parse/stack/generators/templates/model_session.rb +1 -1
  93. data/lib/parse/stack/generators/templates/parse.rb +1 -1
  94. data/lib/parse/stack/generators/templates/webhooks.rb +1 -1
  95. data/lib/parse/stack/version.rb +1 -1
  96. data/lib/parse/stack.rb +375 -73
  97. data/lib/parse/two_factor_auth/user_extension.rb +5 -2
  98. data/lib/parse/vector_search.rb +341 -0
  99. data/parse-stack-next.gemspec +10 -9
  100. data/scripts/docker/docker-compose.test.yml +18 -0
  101. data/scripts/start-parse.sh +6 -0
  102. data/scripts/vector_prototype/create_vector_index.js +105 -0
  103. data/scripts/vector_prototype/fetch_embeddings.py +241 -0
  104. data/scripts/vector_prototype/fixture_manifest.json +9 -0
  105. data/scripts/vector_prototype/query_prototype.rb +84 -0
  106. data/scripts/vector_prototype/run.sh +34 -0
  107. metadata +77 -5
  108. data/parse-stack.png +0 -0
@@ -0,0 +1,285 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../../vector_search"
5
+ require_relative "../../embeddings"
6
+
7
+ module Parse
8
+ module Core
9
+ # Class-level `find_similar` wrapper around {Parse::VectorSearch.search}
10
+ # for any Parse::Object subclass that has declared at least one
11
+ # `:vector` property.
12
+ #
13
+ # The wrapper handles three things the low-level entry point doesn't:
14
+ #
15
+ # 1. **Field resolution.** Defaults to the subclass's single
16
+ # `:vector` property; raises if the class has none, requires
17
+ # explicit `field:` if it has more than one.
18
+ # 2. **Declared-dimension validation.** Compares the query vector's
19
+ # length against the `dimensions:` declared on the property,
20
+ # so callers get "expected 1536, got 768" instead of an Atlas-
21
+ # side error after a round-trip.
22
+ # 3. **Index auto-discovery.** Looks up the Atlas vectorSearch
23
+ # index covering the field via
24
+ # {Parse::AtlasSearch::IndexCatalog.find_vector_index} when no
25
+ # explicit `index:` kwarg is given.
26
+ #
27
+ # ACL/CLP enforcement is inherited from {Parse::VectorSearch.search}
28
+ # (which routes through {Parse::MongoDB} — REST `/aggregate` is
29
+ # master-key-only and bypasses ACL/CLP, see CLAUDE.md). The full
30
+ # scope-kwarg surface (`session_token:`, `master:`, `acl_user:`,
31
+ # `acl_role:`) is forwarded as-is.
32
+ #
33
+ # @example default field, default index
34
+ # WikiArticle.find_similar(vector: query_embedding, k: 5)
35
+ #
36
+ # @example explicit field + post-filter, scoped to a session
37
+ # Document.find_similar(
38
+ # vector: embed.call("ruby parse"),
39
+ # field: :body_embedding,
40
+ # k: 10,
41
+ # filter: { tag: "ruby" },
42
+ # session_token: user.session_token,
43
+ # )
44
+ module VectorSearchable
45
+ # Raised when the calling class has no `:vector` property to
46
+ # search against. Distinct from {Parse::VectorSearch::InvalidQueryVector}
47
+ # because the misuse is at the class level, not the query level.
48
+ class NoVectorProperty < ArgumentError; end
49
+
50
+ # Raised when a class declares more than one `:vector` property
51
+ # and the caller didn't pass `field:` to disambiguate.
52
+ class AmbiguousVectorField < ArgumentError; end
53
+
54
+ # Raised when no Atlas vectorSearch index covers the requested
55
+ # field and the caller didn't pass an explicit `index:` kwarg.
56
+ class IndexNotResolved < ArgumentError; end
57
+
58
+ # Raised by the `find_similar(text:)` overload when the resolved
59
+ # `:vector` property has no `provider:` (and therefore no way to
60
+ # turn `text:` into a query vector). Distinct from
61
+ # {Parse::Embeddings::ProviderNotRegistered} (registry miss) — this
62
+ # is a class-declaration miss: the field was declared without
63
+ # binding it to a provider at the property level.
64
+ class EmbedderNotConfigured < ArgumentError; end
65
+
66
+ # Find documents whose declared `:vector` property is closest to
67
+ # `vector:` under the Atlas vectorSearch index's similarity
68
+ # function.
69
+ #
70
+ # @param vector [Array<Float>, Parse::Vector, nil] the query
71
+ # embedding. Mutually exclusive with `text:` — exactly one of the
72
+ # two must be given.
73
+ # @param text [String, nil] natural-language query. When given, the
74
+ # resolved field's declared `provider:` is looked up via
75
+ # {Parse::Embeddings.provider}, used to embed `[text]` with
76
+ # `input_type: :search_query`, and the resulting vector is used
77
+ # in place of `vector:`. Requires the property to have been
78
+ # declared with `provider:` metadata.
79
+ # @param k [Integer] number of hits to return. Default 10.
80
+ # @param field [Symbol, String, nil] the `:vector` property to
81
+ # search. Auto-resolves when the class has exactly one
82
+ # `:vector` property.
83
+ # @param filter [Hash, nil] post-`$vectorSearch` `$match` filter.
84
+ # @param vector_filter [Hash, nil] Atlas-native pre-search filter
85
+ # (fields must be declared `type: "filter"` in the index).
86
+ # @param index [String, nil] explicit vectorSearch index name.
87
+ # Skips auto-discovery when given.
88
+ # @param num_candidates [Integer, nil] HNSW search width.
89
+ # @param max_time_ms [Integer, nil] server-side timeout.
90
+ # @param raw [Boolean] when true return the raw Mongo documents
91
+ # (each enriched with `_vscore`); when false (default) build
92
+ # instances of the calling class and attach `vector_score`.
93
+ # @param scope_opts [Hash] ACL/CLP scope kwargs forwarded to
94
+ # {Parse::VectorSearch.search}: `session_token:`, `master:`,
95
+ # `acl_user:`, `acl_role:`.
96
+ # @return [Array<Parse::Object>, Array<Hash>] hits in
97
+ # descending-similarity order. Each instance responds to
98
+ # `vector_score` (the Atlas `vectorSearchScore`).
99
+ # @raise [NoVectorProperty] when the class has no `:vector`
100
+ # property.
101
+ # @raise [AmbiguousVectorField] when the class has more than one
102
+ # `:vector` property and `field:` was omitted.
103
+ # @raise [Parse::VectorSearch::InvalidQueryVector] when the query
104
+ # vector's shape doesn't match the declared dimensions.
105
+ # @raise [IndexNotResolved] when no covering vectorSearch index
106
+ # exists and no explicit `index:` was given.
107
+ # @raise [ArgumentError] when neither `vector:` nor `text:` is
108
+ # given, or both are given.
109
+ # @raise [EmbedderNotConfigured] when `text:` is given but the
110
+ # resolved field's property has no `provider:` declared.
111
+ # @raise [Parse::Embeddings::ProviderNotRegistered] when the
112
+ # declared `provider:` was never registered via
113
+ # {Parse::Embeddings.register}.
114
+ # @raise [Parse::Embeddings::InvalidResponseError] when the
115
+ # registered provider returns a payload that is not a single
116
+ # vector (defense-in-depth above {Parse::Embeddings::Provider#validate_response!}).
117
+ #
118
+ # @note When `text:` is given, the text is sent over the wire to
119
+ # the embedding provider (e.g. OpenAI). Operators that enable
120
+ # global Faraday request logging on the embedding connection
121
+ # will capture the full query text in the JSON request body.
122
+ # Treat `text:` as user-visible content for log-handling
123
+ # purposes.
124
+ # @note The provider is responsible for bounding its own request
125
+ # timeout. {Parse::Embeddings::OpenAI} self-bounds at 30 s read
126
+ # / 5 s connect with capped retries. Custom providers MUST
127
+ # self-bound — `find_similar` does not impose a wall-clock
128
+ # deadline on the embed step.
129
+ def find_similar(vector: nil, text: nil, k: 10, field: nil, filter: nil,
130
+ vector_filter: nil, index: nil,
131
+ num_candidates: nil, max_time_ms: nil, raw: false,
132
+ **scope_opts)
133
+ if vector.nil? && text.nil?
134
+ raise ArgumentError,
135
+ "#{self}.find_similar: must pass either `vector:` or `text:`."
136
+ end
137
+ if !vector.nil? && !text.nil?
138
+ raise ArgumentError,
139
+ "#{self}.find_similar: pass either `vector:` or `text:`, not both."
140
+ end
141
+
142
+ resolved_field = resolve_vector_field!(field)
143
+ declared_dims = vector_properties.dig(resolved_field, :dimensions)
144
+
145
+ query_vector =
146
+ if text.nil?
147
+ coerce_query_vector(vector)
148
+ else
149
+ embed_query_text!(text, resolved_field)
150
+ end
151
+ Parse::VectorSearch.validate_query_vector!(query_vector, dimensions: declared_dims)
152
+
153
+ index_name = resolve_vector_index!(resolved_field, index)
154
+
155
+ raw_hits = Parse::VectorSearch.search(
156
+ parse_class,
157
+ field: resolved_field,
158
+ query_vector: query_vector,
159
+ k: k,
160
+ num_candidates: num_candidates,
161
+ filter: filter,
162
+ vector_filter: vector_filter,
163
+ index: index_name,
164
+ max_time_ms: max_time_ms,
165
+ **scope_opts,
166
+ )
167
+
168
+ return raw_hits if raw
169
+ build_vector_hits(raw_hits)
170
+ end
171
+
172
+ private
173
+
174
+ def resolve_vector_field!(field)
175
+ declared = vector_properties.keys
176
+ if field
177
+ sym = field.to_sym
178
+ unless declared.include?(sym)
179
+ raise NoVectorProperty,
180
+ "#{self}.find_similar: field :#{sym} is not a :vector property " \
181
+ "(declared :vector fields: #{declared.inspect})."
182
+ end
183
+ return sym
184
+ end
185
+ if declared.length == 1
186
+ return declared.first
187
+ end
188
+ if declared.empty?
189
+ raise NoVectorProperty,
190
+ "#{self}.find_similar: no :vector property declared on this class."
191
+ end
192
+ raise AmbiguousVectorField,
193
+ "#{self}.find_similar: class declares multiple :vector properties " \
194
+ "(#{declared.inspect}); pass `field:` to disambiguate."
195
+ end
196
+
197
+ # Maximum bytes accepted for the `text:` overload. 256 KiB is well
198
+ # above any reasonable embedding-model token budget (8K tokens ≈
199
+ # 32 KB UTF-8) and keeps a runaway caller from shipping multi-MB
200
+ # bodies to the provider — and from filling operator error-trackers
201
+ # with the captured request body when the provider eventually 400s.
202
+ MAX_QUERY_TEXT_BYTES = 256 * 1024
203
+
204
+ # Embed a natural-language query against the provider declared on
205
+ # the resolved `:vector` property. Validates the text input here
206
+ # (rather than letting the provider raise its own message) so the
207
+ # error trace points at `find_similar`, not at HTTP-layer code.
208
+ def embed_query_text!(text, resolved_field)
209
+ unless text.is_a?(String)
210
+ raise ArgumentError,
211
+ "#{self}.find_similar: `text:` must be a String (got #{text.class})."
212
+ end
213
+ if text.empty?
214
+ raise ArgumentError,
215
+ "#{self}.find_similar: `text:` is empty."
216
+ end
217
+ if text.bytesize > MAX_QUERY_TEXT_BYTES
218
+ raise ArgumentError,
219
+ "#{self}.find_similar: `text:` exceeds #{MAX_QUERY_TEXT_BYTES} bytes " \
220
+ "(#{text.bytesize}); embedding providers will reject it. Chunk the " \
221
+ "input client-side before calling find_similar(text:)."
222
+ end
223
+ provider_name = vector_properties.dig(resolved_field, :provider)
224
+ if provider_name.nil?
225
+ raise EmbedderNotConfigured,
226
+ "#{self}.find_similar: property :#{resolved_field} has no `provider:` " \
227
+ "declared; cannot embed `text:`. Declare `provider: :openai` (or other) " \
228
+ "on the property, or pass an explicit `vector:`."
229
+ end
230
+ provider = Parse::Embeddings.provider(provider_name)
231
+ vectors = provider.embed_text([text], input_type: :search_query)
232
+ unless vectors.is_a?(Array) && vectors.length == 1 && vectors.first.is_a?(Array)
233
+ raise Parse::Embeddings::InvalidResponseError,
234
+ "#{self}.find_similar: provider #{provider_name.inspect} did not return " \
235
+ "a single vector for `text:` (got #{vectors.inspect[0, 80]})."
236
+ end
237
+ vectors.first
238
+ end
239
+
240
+ def coerce_query_vector(vector)
241
+ case vector
242
+ when Parse::Vector then vector.to_a
243
+ when Array then vector
244
+ else
245
+ raise Parse::VectorSearch::InvalidQueryVector,
246
+ "vector: must be an Array<Float> or Parse::Vector (got #{vector.class})."
247
+ end
248
+ end
249
+
250
+ def resolve_vector_index!(field, explicit_index)
251
+ return explicit_index if explicit_index && !explicit_index.to_s.empty?
252
+ begin
253
+ require_relative "../../atlas_search"
254
+ rescue LoadError
255
+ raise IndexNotResolved,
256
+ "#{self}.find_similar: no index: given and Parse::AtlasSearch " \
257
+ "could not be loaded; pass an explicit index: kwarg."
258
+ end
259
+ idx = Parse::AtlasSearch::IndexCatalog.find_vector_index(parse_class, field: field)
260
+ if idx.nil?
261
+ raise IndexNotResolved,
262
+ "#{self}.find_similar: no vectorSearch index found covering " \
263
+ "#{parse_class}.#{field}; pass index: explicitly or create one " \
264
+ "via Parse::AtlasSearch::IndexCatalog.create_index."
265
+ end
266
+ (idx["name"] || idx[:name]).to_s
267
+ end
268
+
269
+ def build_vector_hits(raw_hits)
270
+ return [] if raw_hits.nil? || raw_hits.empty?
271
+ converted = Parse::MongoDB.convert_documents_to_parse(raw_hits, parse_class)
272
+ converted.each_with_index.map do |doc, idx|
273
+ obj = Parse::Object.build(doc, parse_class)
274
+ next nil unless obj
275
+ score = raw_hits[idx]["_vscore"] || raw_hits[idx][:_vscore]
276
+ # `vector_score` reader is defined once on Parse::Object — see
277
+ # lib/parse/model/object.rb — so we only need to set the ivar
278
+ # here. No per-row singleton methods.
279
+ obj.instance_variable_set(:@_vector_score, score) if score
280
+ obj
281
+ end.compact
282
+ end
283
+ end
284
+ end
285
+ end
@@ -221,8 +221,7 @@ module Parse
221
221
 
222
222
  size_cap = max_remote_size
223
223
  timeout = remote_timeout
224
- URI.open(uri,
225
- read_timeout: timeout,
224
+ uri.open(read_timeout: timeout,
226
225
  open_timeout: timeout,
227
226
  redirect: false,
228
227
  content_length_proc: ->(len) {
@@ -479,10 +478,23 @@ module Parse
479
478
  end
480
479
 
481
480
  # Save the file by uploading it to Parse and creating a file pointer.
481
+ # @param session_token [String, nil] thread an authenticated user's
482
+ # session token through the upload. Required when the SDK is running
483
+ # in client mode against a Parse Server with
484
+ # `fileUpload.enableForAuthenticatedUser` on (the typical safe
485
+ # configuration). When nil, the upload uses whatever auth the
486
+ # default client carries — which for client-mode builds is anonymous.
487
+ # @param use_master_key [Boolean, nil] explicitly opt in or out of
488
+ # master-key auth for this upload. Defaults to the client's
489
+ # configured behavior. Pass `false` in client-mode code to assert
490
+ # that no master key is smuggled into the upload.
482
491
  # @return [Boolean] true if successfully uploaded and saved.
483
- def save
492
+ def save(session_token: nil, use_master_key: nil)
484
493
  unless saved? || @contents.nil? || @name.nil?
485
- response = client.create_file(@name, @contents, @mime_type)
494
+ opts = {}
495
+ opts[:session_token] = session_token unless session_token.nil?
496
+ opts[:use_master_key] = use_master_key unless use_master_key.nil?
497
+ response = client.create_file(@name, @contents, @mime_type, **opts)
486
498
  unless response.error?
487
499
  result = response.result
488
500
  @name = result[FIELD_NAME] || File.basename(result[FIELD_URL])
@@ -102,12 +102,17 @@ module Parse
102
102
  # names used in Parse, we will need to have a dynamic lookup system where
103
103
  # when a parse class name received, we go through all of our subclasses to determine
104
104
  # which Parse::Object subclass is responsible for handling this Parse table class.
105
- # we use @@model_cache to cache the results of the algorithm since we do this frequently
106
- # when encoding and decoding objects.
107
- # @!visibility private
108
- @@model_cache = {}
105
+ # The cache is held as a class-instance variable on the singleton (rather than a
106
+ # `@@class_var`) and guarded by a Mutex so concurrent encode/decode paths cannot
107
+ # tear the underlying Hash. Class-instance state also keeps the cache out of any
108
+ # subclass's reach, matching the per-class-state convention used elsewhere in
109
+ # the SDK (see Parse::ACLScope `@no_acl_warned`).
110
+ @model_cache = {}
111
+ @model_cache_mutex = Mutex.new
109
112
 
110
113
  class << self
114
+ # @!visibility private
115
+ attr_reader :model_cache, :model_cache_mutex
111
116
  # @!attribute self.raise_on_save_failure
112
117
  # By default, we return `true` or `false` for save and destroy operations.
113
118
  # If you prefer to have `Parse::Object` raise an exception instead, you
@@ -179,13 +184,24 @@ module Parse
179
184
  # legitimate findables. Rescue per-descendant so the iteration
180
185
  # continues past the unnamed-and-default-parse_class case while
181
186
  # still considering anonymous-but-overridden ones.
182
- @@model_cache[str] ||= Parse::Object.descendants.find do |f|
183
- begin
184
- cls = f.parse_class
185
- rescue StandardError
186
- next false
187
+ # Reference Parse::Model directly: this class method is inherited by
188
+ # subclasses (e.g. Parse::Object.find_class), so a bare `@model_cache`
189
+ # would resolve on the subclass singleton — which has no cache. The
190
+ # cache lives on Parse::Model itself.
191
+ Parse::Model.model_cache_mutex.synchronize do
192
+ cached = Parse::Model.model_cache[str]
193
+ return cached if cached
194
+
195
+ result = Parse::Object.descendants.find do |f|
196
+ begin
197
+ cls = f.parse_class
198
+ rescue StandardError
199
+ next false
200
+ end
201
+ cls == str || cls == "_#{str}"
187
202
  end
188
- cls == str || cls == "_#{str}"
203
+ Parse::Model.model_cache[str] = result if result
204
+ result
189
205
  end
190
206
  end
191
207
  end
@@ -23,6 +23,7 @@ require_relative "date"
23
23
  require_relative "time_zone"
24
24
  require_relative "phone"
25
25
  require_relative "email"
26
+ require_relative "vector"
26
27
  require_relative "acl"
27
28
  require_relative "clp"
28
29
  require_relative "push"
@@ -35,6 +36,8 @@ require_relative "core/describe"
35
36
  require_relative "core/indexing"
36
37
  require_relative "core/search_indexing"
37
38
  require_relative "core/properties"
39
+ require_relative "core/vector_searchable"
40
+ require_relative "core/embed_managed"
38
41
  require_relative "core/errors"
39
42
  require_relative "core/builder"
40
43
  require_relative "core/enhanced_change_tracking"
@@ -117,7 +120,7 @@ module Parse
117
120
  #
118
121
  # All columns in a Parse object are considered a type of property (ex. string, numbers, arrays, etc)
119
122
  # except in two cases - Pointers and Relations. For a detailed discussion of properties, see
120
- # The {https://github.com/modernistik/parse-stack#defining-properties Defining Properties} section.
123
+ # The {https://github.com/neurosynq/parse-stack-next#defining-properties Defining Properties} section.
121
124
  #
122
125
  # Associations:
123
126
  #
@@ -151,11 +154,33 @@ module Parse
151
154
  extend Core::Describe
152
155
  extend Core::Indexing
153
156
  extend Core::SearchIndexing
157
+ extend Core::VectorSearchable
158
+ include Core::EmbedManaged
154
159
  include Core::Fetching
155
160
  include Core::Actions
156
161
  # @!visibility private
157
162
  BASE_OBJECT_CLASS = "Parse::Object".freeze
158
163
 
164
+ # Search/vector-search result accessors. Populated by
165
+ # `Parse::AtlasSearch.process_search_results` and
166
+ # `Parse::Core::VectorSearchable.build_vector_hits` via
167
+ # `instance_variable_set`. Defined here once instead of per-result
168
+ # via `define_singleton_method` so high-k result sets don't inflate
169
+ # a singleton class per row, and so a user-defined override on a
170
+ # subclass can't silently desync from the ivar.
171
+ #
172
+ # Each returns nil unless the object was returned from the
173
+ # corresponding search path.
174
+ #
175
+ # @return [Float, nil] vectorSearch relevance score.
176
+ def vector_score; @_vector_score; end
177
+
178
+ # @return [Float, nil] Atlas Search relevance score.
179
+ def search_score; @_search_score; end
180
+
181
+ # @return [Hash, nil] Atlas Search highlights blob.
182
+ def search_highlights; @_search_highlights; end
183
+
159
184
  # @return [Model::TYPE_OBJECT]
160
185
  def __type; Parse::Model::TYPE_OBJECT; end
161
186
 
@@ -365,6 +390,7 @@ module Parse
365
390
  def default_acls
366
391
  @default_acls ||= case acl_policy_setting
367
392
  when :public, :owner_else_public then Parse::ACL.everyone
393
+ when :public_read, :owner_but_public_read then Parse::ACL.everyone(true, false)
368
394
  when :private, :owner_else_private then Parse::ACL.private
369
395
  else Parse::ACL.everyone
370
396
  end
@@ -431,7 +457,7 @@ module Parse
431
457
  end
432
458
 
433
459
  # Valid ACL policies that can be passed to {acl_policy}.
434
- VALID_ACL_POLICIES = [:public, :private, :owner_else_public, :owner_else_private].freeze
460
+ VALID_ACL_POLICIES = [:public, :public_read, :private, :owner_else_public, :owner_else_private, :owner_but_public_read].freeze
435
461
 
436
462
  # Declarative ACL policy applied to newly-created instances of this class.
437
463
  # The policy is resolved at save time so that explicit ACL changes by the
@@ -443,9 +469,16 @@ module Parse
443
469
  # 2. Owner pointer resolved from the declared `owner:` field → owner R/W only
444
470
  # 3. The else-half of the policy: `:public` → public R/W, `:private` → master-key only
445
471
  #
446
- # @param policy [Symbol] one of `:public`, `:private`, `:owner_else_public`, `:owner_else_private`.
472
+ # @param policy [Symbol] one of `:public`, `:public_read`, `:private`,
473
+ # `:owner_else_public`, `:owner_else_private`, `:owner_but_public_read`.
474
+ # `:public_read` stamps `{"*": {"read": true}}` — anyone can read, no
475
+ # one can write through ACL (only the master key can mutate). Useful
476
+ # for catalog/lookup tables. `:owner_but_public_read` stamps the
477
+ # resolved owner with R/W AND grants public read in the same ACL —
478
+ # useful for publicly-viewable content with a single authoring user;
479
+ # falls back to `:public_read` semantics when no owner resolves.
447
480
  # @param owner [Symbol,nil] the name of the property/belongs_to whose pointer designates the owner user.
448
- # Only meaningful for `:owner_else_*` policies.
481
+ # Only meaningful for `:owner_*` policies.
449
482
  # @example
450
483
  # class Post < Parse::Object
451
484
  # acl_policy :owner_else_private, owner: :author
@@ -488,10 +521,14 @@ module Parse
488
521
  "declare a belongs_to pointer to the owning user."
489
522
  end
490
523
  if owner && !policy.to_s.start_with?("owner_")
491
- warn "[#{self}] `owner:` is ignored when acl_policy is #{policy.inspect}; only :owner_else_public and :owner_else_private use it."
524
+ warn "[#{self}] `owner:` is ignored when acl_policy is #{policy.inspect}; only :owner_else_public, :owner_else_private, and :owner_but_public_read use it."
492
525
  end
493
526
  if owner.nil? && policy.to_s.start_with?("owner_")
494
- fallback = (policy == :owner_else_public) ? "public R/W" : "master-key-only"
527
+ fallback = case policy
528
+ when :owner_else_public then "public R/W"
529
+ when :owner_but_public_read then "public read only"
530
+ else "master-key-only"
531
+ end
495
532
  warn "[#{self}] acl_policy #{policy.inspect} declared without `owner:` field; ACL resolution will always use the fallback (#{fallback}). Pass `as:` at construction to override."
496
533
  end
497
534
  @acl_policy_setting = policy
@@ -1180,6 +1217,20 @@ module Parse
1180
1217
  end
1181
1218
  end
1182
1219
 
1220
+ # `:vector` fields are excluded from serialization by default —
1221
+ # embeddings are large (often 1024–4096 floats), they leak ML
1222
+ # signal to clients, and they round-trip through the dedicated
1223
+ # embed/find_similar pipelines rather than the standard REST
1224
+ # save/find. Pass `include_vectors: true` to opt back in (e.g.,
1225
+ # for tests or internal mongo-direct bulk writes).
1226
+ unless opts[:include_vectors] == true
1227
+ vector_fields = self.class.respond_to?(:fields) ? self.class.fields(:vector).keys.map(&:to_s) : []
1228
+ if vector_fields.any?
1229
+ except = Array(opts[:except]).map(&:to_s) | vector_fields
1230
+ opts = opts.merge(except: except)
1231
+ end
1232
+ end
1233
+
1183
1234
  # When :only is specified without :strict, automatically include identification fields
1184
1235
  # so the serialized object can be properly identified
1185
1236
  if opts[:only] && !opts[:strict]
@@ -1807,6 +1858,8 @@ module Parse
1807
1858
  target_acl = case policy
1808
1859
  when :public
1809
1860
  Parse::ACL.everyone(true, true)
1861
+ when :public_read
1862
+ Parse::ACL.everyone(true, false)
1810
1863
  when :private
1811
1864
  Parse::ACL.private
1812
1865
  when :owner_else_public
@@ -1825,6 +1878,10 @@ module Parse
1825
1878
  else
1826
1879
  Parse::ACL.private
1827
1880
  end
1881
+ when :owner_but_public_read
1882
+ acl = Parse::ACL.everyone(true, false)
1883
+ acl.apply(owner_id, true, true) if owner_id
1884
+ acl
1828
1885
  end
1829
1886
 
1830
1887
  # Only re-stamp if the resolved ACL differs from the init-time stamp;
@@ -109,6 +109,20 @@ module Parse
109
109
  # @return [Model::TYPE_POINTER]
110
110
  def __type; Parse::Model::TYPE_POINTER; end
111
111
 
112
+ # Search/vector-search result accessors. Defined here (instead of
113
+ # only on Parse::Object) so that hydration paths which fall back to
114
+ # a Pointer — e.g. Atlas Search results whose `className` has no
115
+ # corresponding Ruby subclass loaded — still surface the score and
116
+ # highlights attached by `Parse::AtlasSearch.process_search_results`
117
+ # / `Parse::Core::VectorSearchable.build_vector_hits`. Each returns
118
+ # nil unless the instance came from the corresponding search path.
119
+ # @return [Float, nil]
120
+ def vector_score; @_vector_score; end
121
+ # @return [Float, nil]
122
+ def search_score; @_search_score; end
123
+ # @return [Hash, nil]
124
+ def search_highlights; @_search_highlights; end
125
+
112
126
  alias_method :className, :parse_class
113
127
  # A Parse object as a className field and objectId. In ruby, we will use the
114
128
  # id attribute method, but for usability, we will also alias it to objectId
@@ -289,9 +303,9 @@ module Parse
289
303
  # @param includes [Array<String>, nil] optional list of pointer fields to expand.
290
304
  # @return [Parse::Object] the fetched Parse::Object, nil otherwise.
291
305
  # @example Fetch pointer with caching
292
- # capture = capture_pointer.fetch_cache!
306
+ # post = post_pointer.fetch_cache!
293
307
  # @example Partial fetch with caching
294
- # capture = capture_pointer.fetch_cache!(keys: [:title, :status])
308
+ # post = post_pointer.fetch_cache!(keys: [:title, :status])
295
309
  # @see #fetch
296
310
  def fetch_cache!(keys: nil, includes: nil)
297
311
  fetch(keys: keys, includes: includes, cache: true)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "object"
2
4
 
3
5
  # Simple include to use short verion of core class names
@@ -26,11 +26,11 @@ module Parse
26
26
  # validates :username, uniqueness: { case_sensitive: false }
27
27
  # end
28
28
  #
29
- # @example Scoped uniqueness (unique within an organization)
29
+ # @example Scoped uniqueness (unique within a tenant)
30
30
  # class Employee < Parse::Object
31
31
  # property :employee_id, :string
32
- # belongs_to :organization
33
- # validates :employee_id, uniqueness: { scope: :organization }
32
+ # belongs_to :tenant
33
+ # validates :employee_id, uniqueness: { scope: :tenant }
34
34
  # end
35
35
  #
36
36
  # @example With custom message