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.
- checksums.yaml +4 -4
- data/.bundle/config +2 -0
- data/.env.sample +17 -3
- data/.github/workflows/codeql.yml +44 -0
- data/.github/workflows/docs.yml +39 -0
- data/.github/workflows/release.yml +32 -0
- data/.github/workflows/ruby.yml +8 -6
- data/.gitignore +4 -0
- data/.vscode/settings.json +3 -0
- data/CHANGELOG.md +305 -72
- data/Gemfile.lock +10 -3
- data/LICENSE.txt +1 -1
- data/README.md +190 -219
- data/Rakefile +1 -1
- data/SECURITY.md +30 -0
- data/assets/parse-stack-next-avatar.png +0 -0
- data/assets/parse-stack-next-avatar.svg +37 -0
- data/assets/parse-stack-next-banner.png +0 -0
- data/assets/parse-stack-next-banner.svg +45 -0
- data/assets/parse-stack-next-social-preview.png +0 -0
- data/docs/atlas_vector_search_guide.md +511 -0
- data/docs/client_sdk_guide.md +1320 -0
- data/docs/mcp_guide.md +225 -104
- data/docs/mongodb_direct_guide.md +21 -4
- data/docs/usage_guide.md +585 -0
- data/examples/transaction_example.rb +28 -28
- data/lib/parse/acl_scope.rb +2 -2
- data/lib/parse/agent/mcp_rack_app.rb +184 -16
- data/lib/parse/agent/metadata_dsl.rb +16 -16
- data/lib/parse/agent/pipeline_validator.rb +28 -1
- data/lib/parse/agent/prompts.rb +5 -5
- data/lib/parse/agent/tools.rb +287 -14
- data/lib/parse/agent.rb +209 -12
- data/lib/parse/api/analytics.rb +27 -5
- data/lib/parse/api/files.rb +6 -2
- data/lib/parse/api/push.rb +21 -4
- data/lib/parse/api/server.rb +59 -0
- data/lib/parse/api/users.rb +26 -2
- data/lib/parse/atlas_search/index_manager.rb +84 -0
- data/lib/parse/atlas_search.rb +37 -9
- data/lib/parse/cache/pool.rb +88 -0
- data/lib/parse/cache/redis.rb +249 -0
- data/lib/parse/client/body_builder.rb +94 -0
- data/lib/parse/client/caching.rb +109 -9
- data/lib/parse/client/response.rb +27 -0
- data/lib/parse/client.rb +74 -3
- data/lib/parse/console.rb +203 -0
- data/lib/parse/embeddings/cohere.rb +484 -0
- data/lib/parse/embeddings/fixture.rb +130 -0
- data/lib/parse/embeddings/jina.rb +454 -0
- data/lib/parse/embeddings/local_http.rb +492 -0
- data/lib/parse/embeddings/openai.rb +520 -0
- data/lib/parse/embeddings/provider.rb +264 -0
- data/lib/parse/embeddings/qwen.rb +431 -0
- data/lib/parse/embeddings/voyage.rb +550 -0
- data/lib/parse/embeddings.rb +225 -0
- data/lib/parse/graphql/scalars.rb +53 -0
- data/lib/parse/graphql/type_generator.rb +264 -0
- data/lib/parse/graphql.rb +48 -0
- data/lib/parse/live_query/client.rb +24 -5
- data/lib/parse/live_query/subscription.rb +17 -6
- data/lib/parse/live_query.rb +9 -4
- data/lib/parse/model/associations/collection_proxy.rb +2 -2
- data/lib/parse/model/associations/has_many.rb +32 -1
- data/lib/parse/model/associations/has_one.rb +17 -0
- data/lib/parse/model/associations/pointer_collection_proxy.rb +3 -3
- data/lib/parse/model/classes/user.rb +307 -11
- data/lib/parse/model/clp.rb +1 -1
- data/lib/parse/model/core/create_lock.rb +14 -2
- data/lib/parse/model/core/embed_managed.rb +296 -0
- data/lib/parse/model/core/fetching.rb +4 -4
- data/lib/parse/model/core/indexing.rb +53 -14
- data/lib/parse/model/core/parse_reference.rb +3 -3
- data/lib/parse/model/core/properties.rb +70 -1
- data/lib/parse/model/core/querying.rb +57 -1
- data/lib/parse/model/core/vector_searchable.rb +285 -0
- data/lib/parse/model/file.rb +16 -4
- data/lib/parse/model/model.rb +26 -10
- data/lib/parse/model/object.rb +63 -6
- data/lib/parse/model/pointer.rb +16 -2
- data/lib/parse/model/shortnames.rb +2 -0
- data/lib/parse/model/validations/uniqueness_validator.rb +3 -3
- data/lib/parse/model/vector.rb +102 -0
- data/lib/parse/mongodb.rb +90 -8
- data/lib/parse/pipeline_security.rb +59 -2
- data/lib/parse/query/constraints.rb +16 -14
- data/lib/parse/query/ordering.rb +1 -1
- data/lib/parse/query.rb +137 -64
- data/lib/parse/stack/generators/templates/model.erb +2 -2
- data/lib/parse/stack/generators/templates/model_installation.rb +1 -1
- data/lib/parse/stack/generators/templates/model_role.rb +1 -1
- data/lib/parse/stack/generators/templates/model_session.rb +1 -1
- data/lib/parse/stack/generators/templates/parse.rb +1 -1
- data/lib/parse/stack/generators/templates/webhooks.rb +1 -1
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +375 -73
- data/lib/parse/two_factor_auth/user_extension.rb +5 -2
- data/lib/parse/vector_search.rb +341 -0
- data/parse-stack-next.gemspec +10 -9
- data/scripts/docker/docker-compose.test.yml +18 -0
- data/scripts/start-parse.sh +6 -0
- data/scripts/vector_prototype/create_vector_index.js +105 -0
- data/scripts/vector_prototype/fetch_embeddings.py +241 -0
- data/scripts/vector_prototype/fixture_manifest.json +9 -0
- data/scripts/vector_prototype/query_prototype.rb +84 -0
- data/scripts/vector_prototype/run.sh +34 -0
- metadata +77 -5
- 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
|
data/lib/parse/model/file.rb
CHANGED
|
@@ -221,8 +221,7 @@ module Parse
|
|
|
221
221
|
|
|
222
222
|
size_cap = max_remote_size
|
|
223
223
|
timeout = remote_timeout
|
|
224
|
-
|
|
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
|
-
|
|
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])
|
data/lib/parse/model/model.rb
CHANGED
|
@@ -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
|
-
#
|
|
106
|
-
#
|
|
107
|
-
#
|
|
108
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
203
|
+
Parse::Model.model_cache[str] = result if result
|
|
204
|
+
result
|
|
189
205
|
end
|
|
190
206
|
end
|
|
191
207
|
end
|
data/lib/parse/model/object.rb
CHANGED
|
@@ -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/
|
|
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`, `:
|
|
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 `:
|
|
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 :
|
|
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 =
|
|
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;
|
data/lib/parse/model/pointer.rb
CHANGED
|
@@ -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
|
-
#
|
|
306
|
+
# post = post_pointer.fetch_cache!
|
|
293
307
|
# @example Partial fetch with caching
|
|
294
|
-
#
|
|
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)
|
|
@@ -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
|
|
29
|
+
# @example Scoped uniqueness (unique within a tenant)
|
|
30
30
|
# class Employee < Parse::Object
|
|
31
31
|
# property :employee_id, :string
|
|
32
|
-
# belongs_to :
|
|
33
|
-
# validates :employee_id, uniqueness: { scope: :
|
|
32
|
+
# belongs_to :tenant
|
|
33
|
+
# validates :employee_id, uniqueness: { scope: :tenant }
|
|
34
34
|
# end
|
|
35
35
|
#
|
|
36
36
|
# @example With custom message
|