parse-stack-next 5.7.6 → 5.8.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/CHANGELOG.md +1148 -0
- data/README.md +39 -23
- data/docs/TEST_SERVER.md +2 -2
- data/docs/acl_clp_guide.md +7 -0
- data/docs/atlas_vector_search_guide.md +181 -13
- data/docs/caching.md +50 -0
- data/docs/client_sdk_guide.md +11 -0
- data/docs/mcp_guide.md +332 -9
- data/docs/mongodb_direct_guide.md +27 -0
- data/docs/usage_guide.md +38 -0
- data/docs/webhooks_guide.md +126 -17
- data/lib/parse/acl_scope.rb +230 -50
- data/lib/parse/agent/approval_gate.rb +0 -0
- data/lib/parse/agent/constraint_translator.rb +42 -15
- data/lib/parse/agent/describe.rb +11 -3
- data/lib/parse/agent/field_names.rb +53 -0
- data/lib/parse/agent/field_policy.rb +74 -0
- data/lib/parse/agent/mcp_deployments.rb +426 -0
- data/lib/parse/agent/mcp_dispatcher.rb +8 -1
- data/lib/parse/agent/mcp_rack_app.rb +424 -45
- data/lib/parse/agent/mcp_server.rb +23 -1
- data/lib/parse/agent/mcp_subscriptions.rb +124 -6
- data/lib/parse/agent/metadata_registry.rb +67 -8
- data/lib/parse/agent/prompt_hardening.rb +9 -3
- data/lib/parse/agent/tools.rb +378 -29
- data/lib/parse/agent.rb +298 -21
- data/lib/parse/api/batch.rb +10 -1
- data/lib/parse/api/schema.rb +23 -4
- data/lib/parse/api/sessions.rb +6 -2
- data/lib/parse/api/users.rb +88 -14
- data/lib/parse/atlas_search/index_manager.rb +5 -1
- data/lib/parse/atlas_search/protected_paths.rb +236 -0
- data/lib/parse/atlas_search.rb +144 -24
- data/lib/parse/authorization.rb +289 -4
- data/lib/parse/cache/sub_cache.rb +26 -0
- data/lib/parse/client/authentication.rb +19 -1
- data/lib/parse/client/batch.rb +264 -35
- data/lib/parse/client/body_builder.rb +21 -0
- data/lib/parse/client/caching.rb +371 -27
- data/lib/parse/client/request.rb +26 -14
- data/lib/parse/client/response.rb +49 -6
- data/lib/parse/client.rb +227 -40
- data/lib/parse/clp_scope.rb +317 -25
- data/lib/parse/console.rb +2 -2
- data/lib/parse/embeddings/voyage.rb +181 -17
- data/lib/parse/graphql/type_generator.rb +3 -0
- data/lib/parse/live_query/client.rb +72 -2
- data/lib/parse/lock_backend.rb +4 -3
- data/lib/parse/model/acl.rb +119 -21
- data/lib/parse/model/associations/belongs_to.rb +25 -3
- data/lib/parse/model/associations/collection_proxy.rb +146 -17
- data/lib/parse/model/associations/has_many.rb +49 -9
- data/lib/parse/model/associations/has_one.rb +3 -1
- data/lib/parse/model/associations/pointer_collection_proxy.rb +145 -17
- data/lib/parse/model/associations/relation_collection_proxy.rb +229 -38
- data/lib/parse/model/bytes.rb +13 -5
- data/lib/parse/model/classes/role.rb +95 -0
- data/lib/parse/model/classes/session.rb +304 -1
- data/lib/parse/model/classes/user.rb +104 -8
- data/lib/parse/model/core/actions.rb +287 -70
- data/lib/parse/model/core/builder.rb +100 -8
- data/lib/parse/model/core/create_lock.rb +27 -2
- data/lib/parse/model/core/describe.rb +2 -0
- data/lib/parse/model/core/fetching.rb +50 -5
- data/lib/parse/model/core/field_guards.rb +16 -8
- data/lib/parse/model/core/pluralized_aliases.rb +8 -4
- data/lib/parse/model/core/properties.rb +488 -39
- data/lib/parse/model/core/querying.rb +7 -0
- data/lib/parse/model/core/schema.rb +5 -3
- data/lib/parse/model/core/search_indexing.rb +63 -0
- data/lib/parse/model/core/vector_searchable.rb +35 -6
- data/lib/parse/model/file.rb +9 -2
- data/lib/parse/model/geopoint.rb +61 -13
- data/lib/parse/model/model.rb +160 -9
- data/lib/parse/model/object.rb +317 -18
- data/lib/parse/model/phone.rb +54 -5
- data/lib/parse/model/pointer.rb +40 -6
- data/lib/parse/model/push.rb +11 -1
- data/lib/parse/mongodb.rb +189 -61
- data/lib/parse/pipeline_security.rb +415 -26
- data/lib/parse/query/constraint.rb +31 -1
- data/lib/parse/query/constraints.rb +89 -59
- data/lib/parse/query/cursor.rb +3 -1
- data/lib/parse/query/operation.rb +62 -8
- data/lib/parse/query/ordering.rb +34 -6
- data/lib/parse/query.rb +1871 -167
- data/lib/parse/retrieval/agent_tool.rb +225 -8
- data/lib/parse/retrieval/benchmark.rb +149 -0
- data/lib/parse/retrieval/profiles.rb +320 -0
- data/lib/parse/retrieval/retriever.rb +10 -1
- data/lib/parse/retrieval.rb +2 -0
- data/lib/parse/schema/search_index_migrator.rb +23 -5
- data/lib/parse/schema.rb +74 -18
- data/lib/parse/stack/tasks.rb +6 -4
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +91 -14
- data/lib/parse/two_factor_auth/user_extension.rb +14 -2
- data/lib/parse/two_factor_auth.rb +11 -0
- data/lib/parse/vector_search/hybrid.rb +110 -33
- data/lib/parse/vector_search/index_definition.rb +237 -0
- data/lib/parse/vector_search.rb +259 -23
- data/lib/parse/webhooks/payload.rb +136 -6
- data/lib/parse/webhooks/replay_protection.rb +58 -20
- data/lib/parse/webhooks.rb +715 -40
- metadata +8 -1
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,1153 @@
|
|
|
1
1
|
## parse-stack-next Changelog
|
|
2
2
|
|
|
3
|
+
### 5.8.1
|
|
4
|
+
|
|
5
|
+
A correctness release for webhooks, sessions, associations, queries, search,
|
|
6
|
+
and agent policies. No new defaults and no API changes; every corrected
|
|
7
|
+
outcome is listed under Behavior Notes.
|
|
8
|
+
|
|
9
|
+
#### Webhooks
|
|
10
|
+
|
|
11
|
+
- **FIXED**: A client create through a `before_save` webhook whose handler
|
|
12
|
+
returns `parse_object` now gets the ACL its class policy resolves, exactly
|
|
13
|
+
as an SDK-side create would. Owner-based policies (`:owner_else_private`,
|
|
14
|
+
the default, plus `:owner_else_public` and `:owner_but_public_read`) take the
|
|
15
|
+
owner from the declared `owner:` field, or from the requesting user when that
|
|
16
|
+
field is empty. Before, a signed-in client got the policy's no-owner
|
|
17
|
+
fallback (`{}` under `:owner_else_private`) and could not read its own
|
|
18
|
+
record. A request without a user, or a master-key request, gets the policy
|
|
19
|
+
fallback. A `_User` class declared with `owner: :self` is owned by the new
|
|
20
|
+
user, never the requester. An ACL the client sent is kept, and an ACL the
|
|
21
|
+
handler sets always wins (through `acl=` or mass assignment), even when it
|
|
22
|
+
equals the class default. A `guard :acl, :master_only` revert on a create
|
|
23
|
+
returns the ACL to the class policy instead of `{}`. A handler-assigned ACL
|
|
24
|
+
is written whatever the handler returns (`parse_object`, `true`, `nil`, or
|
|
25
|
+
a Hash that does not set `ACL` itself).
|
|
26
|
+
- **FIXED**: When a `before_save` handler changes a record, a sub-document the
|
|
27
|
+
client changed is written back as one-level dotted paths (`"meta.count":
|
|
28
|
+
{...}`) instead of the whole field, so concurrent writes to other sub-keys
|
|
29
|
+
survive. A removed sub-key is written as a `Delete`. Dotted keys in a
|
|
30
|
+
returned Hash are folded into the field or `field.sub` write they belong to,
|
|
31
|
+
with operators (`Increment`, `Add`, `AddUnique`, `Remove`, `Delete`) applied
|
|
32
|
+
rather than stored. Hash keys may use Ruby names.
|
|
33
|
+
|
|
34
|
+
#### Sessions and master-key scope
|
|
35
|
+
|
|
36
|
+
- **FIXED**: Deleting `Parse::Session` or `Parse::User` objects, one at a time
|
|
37
|
+
or with a batch `Array#destroy`, drops their identity-cache entries when the
|
|
38
|
+
delete succeeds or reports the row already gone. A denied delete leaves the
|
|
39
|
+
cache alone. A session that does not carry its token or owner (built from
|
|
40
|
+
its objectId alone, fetched without the master key, or fetched with `keys:`
|
|
41
|
+
leaving them out) has them looked up first, in one query per batch: with the
|
|
42
|
+
master key when the client has one, otherwise with the delete's `session:`.
|
|
43
|
+
A lookup that finds no row leaves nothing to forget; only a lookup that
|
|
44
|
+
fails resets the client's identity cache, at most once every 5 seconds per
|
|
45
|
+
client. When the lookup finds no row, the delete uses the owner recorded
|
|
46
|
+
when a `_Session` query or `Parse::Session.session` last fetched that
|
|
47
|
+
session through the same client (only the user id, never the token, in a
|
|
48
|
+
store separate from the identity plane,
|
|
49
|
+
`client.authorization.session_owner_cache`). Without one, a batch delete
|
|
50
|
+
that applied or reported the row gone falls back to the rate-limited reset;
|
|
51
|
+
a denied delete never resets, and a single `destroy` never resets. The
|
|
52
|
+
looked-up token is used only for the delete, is never kept on the object,
|
|
53
|
+
and never enters the response cache. A token resolution in flight when an
|
|
54
|
+
invalidation lands, in this process or, on the Redis identity plane, in any
|
|
55
|
+
other, does not cache its answer: the resolver checks a shared invalidation
|
|
56
|
+
marker before and after writing and evicts its own write when the marker
|
|
57
|
+
moved.
|
|
58
|
+
- **FIXED**: `Parse::User#signup!`, signup on save, and `upgrade_anonymous!`
|
|
59
|
+
store `created_at` / `updated_at` as `Parse::Date` values. Reading
|
|
60
|
+
`updated_at` on a newly signed-up user no longer marks it dirty.
|
|
61
|
+
- **FIXED**: Inside `Parse.without_master_key`, an explicit `master: true`
|
|
62
|
+
passed to `Parse::MongoDB.aggregate`, `Parse::ACLScope`, `Parse::Query`
|
|
63
|
+
direct reads, vector and hybrid search, Atlas Search, or agent tools for
|
|
64
|
+
master-key agents no longer runs as master. The call runs in the public
|
|
65
|
+
scope, as the REST request does once the block strips its master key, and
|
|
66
|
+
the `parse.mongodb.aggregate` notification reports `master_dropped: true`.
|
|
67
|
+
The SDK's own metadata reads keep the master key without lifting the block:
|
|
68
|
+
the class schema and role graph read to enforce a scope, index statistics,
|
|
69
|
+
and the Atlas Search index listing. Before, a schema read refused inside the
|
|
70
|
+
block was cached and denied every scoped mongo-direct read of that class,
|
|
71
|
+
for all callers, for 5 seconds. `Parse::AtlasSearch.faceted_search` raises
|
|
72
|
+
`FacetedSearchNotACLSafe` inside the block, because its bucket counts are
|
|
73
|
+
not ACL-filtered. The metadata marker rides only on the outgoing request
|
|
74
|
+
copy, never on `response.request` or an error's request. A schema read that
|
|
75
|
+
fails inside the block is retried at most every 2 seconds there. A LiveQuery
|
|
76
|
+
admin client that first connected inside the block keeps withholding its
|
|
77
|
+
master key on every reconnect until `allow_master_key_connection!`.
|
|
78
|
+
|
|
79
|
+
#### Agent policies
|
|
80
|
+
|
|
81
|
+
- **FIXED**: A sub-agent's `tools:` and `methods:` filters now only narrow its
|
|
82
|
+
parent's, as `classes:` already did. A child `tools: { only: }` is
|
|
83
|
+
intersected with the parent's allowlist and `except:` sets are unioned; for
|
|
84
|
+
`methods:` the parent's filters stay in force alongside the child's. Before,
|
|
85
|
+
the child's filter replaced the parent's, so a sub-agent could enable a tool
|
|
86
|
+
or `agent_method` within its tier that the parent was not given. A child
|
|
87
|
+
`only:` list with nothing its parent allows raises `ArgumentError` at
|
|
88
|
+
construction.
|
|
89
|
+
- **FIXED**: A sub-agent built with `parent:` uses its parent's approval gate,
|
|
90
|
+
read at call time, unless given its own. Before, it fell back to
|
|
91
|
+
`NullGate`, so a sub-agent built inside an MCP `tools/call` ran tools in
|
|
92
|
+
`require_approval_for` tiers without approval.
|
|
93
|
+
- **FIXED**: Qualified `methods:` entries compare the class by its Parse class
|
|
94
|
+
name, so `"User.x"` and `"_User.x"` (and the `Role`, `Session`, and
|
|
95
|
+
`Installation` aliases) match whichever spelling the entry or the
|
|
96
|
+
`call_method` caller uses. Before, `methods: { except: ["_User.x"] }` could
|
|
97
|
+
be bypassed by calling the method on `"User"`.
|
|
98
|
+
- **FIXED**: A sub-agent can no longer turn on `master_atlas` when its parent
|
|
99
|
+
does not have it. `Parse::Agent.new(parent: scoped_parent, master_atlas:
|
|
100
|
+
true)` raises `ArgumentError`; before, the child's `atlas_faceted_search` ran
|
|
101
|
+
with master-key semantics and returned every row under the parent's scoped
|
|
102
|
+
identity. Omitting `master_atlas` still inherits, and `false` still drops
|
|
103
|
+
it.
|
|
104
|
+
- **FIXED**: The MCP dispatcher snapshots and restores an agent's own approval
|
|
105
|
+
gate, so driving a sub-agent through `tools/call` no longer pins its
|
|
106
|
+
parent's gate onto it. Assigning `approval_gate = nil` makes a sub-agent use
|
|
107
|
+
its parent's gate again.
|
|
108
|
+
- **FIXED**: Qualified `methods:` entries may name a model by its Ruby
|
|
109
|
+
constant (`"Artist.purge"` for a class whose `parse_class` is `"Musician"`)
|
|
110
|
+
and are compared by Parse class name, so such an `except:` entry is no
|
|
111
|
+
longer bypassed through the Parse class name. An entry whose class resolves
|
|
112
|
+
to no loaded model warns at construction.
|
|
113
|
+
- **IMPROVED**: The `parse.agent.tool_call` audit payload adds
|
|
114
|
+
`methods_layers` whenever a sub-agent inherits a `methods:` filter
|
|
115
|
+
(including a single parent layer), and `describe[:methods][:layers]` lists
|
|
116
|
+
every `methods:` filter in force, parent first.
|
|
117
|
+
|
|
118
|
+
#### Associations
|
|
119
|
+
|
|
120
|
+
- **FIXED**: `add!`, `add_unique!`, and `remove!` on a
|
|
121
|
+
`has_many through: :array` collection take the array Parse Server returns,
|
|
122
|
+
as plain array properties already did. A stale local copy is corrected,
|
|
123
|
+
objects already fetched locally are kept, and the field is not marked
|
|
124
|
+
changed. The array is adopted only when every entry is a pointer to the
|
|
125
|
+
declared class; otherwise the operation is applied locally.
|
|
126
|
+
- **FIXED**: Saving a relation collection on its own (`owner.likes.save`)
|
|
127
|
+
sends the staged additions and removals and returns whether they were
|
|
128
|
+
applied. Before, it silently sent nothing.
|
|
129
|
+
- **FIXED**: `Parse::Object#clear_changes!` (also called by `reload!`) drops
|
|
130
|
+
a relation's staged additions and removals and clears array collection
|
|
131
|
+
dirty state. Before, discarded relation changes were sent with the next
|
|
132
|
+
change to that relation. A fetch (`fetch!`, partial fetch, autofetch, with
|
|
133
|
+
or without `preserve_changes:`) keeps a relation's staged additions and
|
|
134
|
+
removals and leaves the relation marked changed, including when the response
|
|
135
|
+
carries the relation's descriptor, so the next save sends them.
|
|
136
|
+
`rollback!` drops a relation's staged additions and removals too, including
|
|
137
|
+
after a fetch.
|
|
138
|
+
- **IMPROVED**: `RelationCollectionProxy#save` accepts `session:` to send the
|
|
139
|
+
staged operations as a given user (`nil` for no session token). Relation
|
|
140
|
+
writes on an owner whose objectId was assigned client-side during a create
|
|
141
|
+
are staged rather than sent until the create returns.
|
|
142
|
+
- **FIXED**: Atomic relation operations (`add!` / `remove!` on a
|
|
143
|
+
`through: :relation` collection) write to the relation's Parse column when
|
|
144
|
+
its name differs from the Ruby name (`has_many :liked_items` maps to
|
|
145
|
+
`likedItems`, or a relation declared with `field:`). Before, they targeted
|
|
146
|
+
the Ruby name.
|
|
147
|
+
|
|
148
|
+
#### Queries
|
|
149
|
+
|
|
150
|
+
- **FIXED**: An OR whose branch has no constraints now matches every row.
|
|
151
|
+
Previously `query | Model.query` and `Parse::Query.or(query, Model.query)`
|
|
152
|
+
dropped that branch and returned only the other side. An empty receiver of
|
|
153
|
+
`|` still starts a new OR, as `or_where` does, so
|
|
154
|
+
`[q1, q2].reduce(Model.query, :|)` builds `q1 OR q2`.
|
|
155
|
+
- **FIXED**: `Model.query(:or => [{...}, {...}])` builds a `$or` group ANDed
|
|
156
|
+
with the query's other constraints, instead of a constraint on a literal
|
|
157
|
+
field named `or`. Branches can be constraint hashes, `Parse::Query` objects
|
|
158
|
+
of the same class, or arrays of constraints.
|
|
159
|
+
- **FIXED**: A constraint that needs the aggregation pipeline or mongo-direct
|
|
160
|
+
(`readable_by`, `:ACL.readable_by`, `:field.array_size`, the mongo-direct geo
|
|
161
|
+
operators) raises `ArgumentError` inside an OR (`:or`, `or_where`, `|`,
|
|
162
|
+
`Parse::Query.or`). Before, the branch was emitted as `{}` and matched every
|
|
163
|
+
row, or lost part of its conditions, including in push targeting, which
|
|
164
|
+
could reach every installation.
|
|
165
|
+
- **FIXED**: `Parse::Query#clone`, `|`, `or_where`, `Parse::Query.or`, `:or`
|
|
166
|
+
branches (including Hash branches with `session:` / `use_master_key:` keys
|
|
167
|
+
or nested scoped queries), and subqueries (`$inQuery`, `$notInQuery`,
|
|
168
|
+
`$select`, `$dontSelect`) keep the query's authority and application:
|
|
169
|
+
session token, a session-bound client from `Parse::Client#become`, scoped
|
|
170
|
+
user or role, master-key flag, read preference, and the Parse application
|
|
171
|
+
the query targets. Queries combined this way must run under the same
|
|
172
|
+
authority against the same application. An unscoped query takes the
|
|
173
|
+
other's authority, and different authorities or applications raise
|
|
174
|
+
`ArgumentError`. Before, a session combined with `scope_to_user` ran as the
|
|
175
|
+
scoped user, a `become`-client query composed into an OR ran as the master
|
|
176
|
+
key, a scoped subquery ran under the outer query's master key, and a clone
|
|
177
|
+
could send one application's session token to the default application.
|
|
178
|
+
Changing the authority or client of a combined query raises and leaves the
|
|
179
|
+
query unchanged. A combined query re-checks its authority before it runs,
|
|
180
|
+
and explicit auth passed to `results_direct`, `count_direct`,
|
|
181
|
+
`distinct_direct`, or `atlas_search` must match it.
|
|
182
|
+
- **FIXED**: LiveQuery subscriptions and push targeting refuse constraints that
|
|
183
|
+
only the aggregation pipeline or mongo-direct can run (`array_size`,
|
|
184
|
+
`set_equals`, `not_empty`, `elem_match`, `readable_by`, mongo-direct geo
|
|
185
|
+
operators). Before, those constraints were dropped and the subscription or
|
|
186
|
+
push reached a wider audience. A subquery with such a constraint is also
|
|
187
|
+
refused, where it used to match every row. `Parse::Query#subscribe` and
|
|
188
|
+
`client.subscribe(query)` run under the query's own session or `become`
|
|
189
|
+
client, and such a subscription is refused with `ArgumentError` when the
|
|
190
|
+
LiveQuery client targets a different Parse application id than the query,
|
|
191
|
+
so one application's session token is never sent to another application's
|
|
192
|
+
LiveQuery server.
|
|
193
|
+
- **FIXED**: A field repeated in `order` is sent once, at its first position
|
|
194
|
+
with the last direction given, matching what MongoDB applies on direct
|
|
195
|
+
reads. String forms such as `"-title"` are parsed as descending, so they
|
|
196
|
+
deduplicate too, and the direct `$sort` stage sorts on the real field.
|
|
197
|
+
`order("title,-plays")` is parsed as two fields.
|
|
198
|
+
- **FIXED**: A bare objectId compared to a declared pointer field
|
|
199
|
+
(`Song.query(:artist => "abc123")`, `:artist.ne`, `:artist.in`) now matches
|
|
200
|
+
on REST, in `Parse::Push` targeting, and in LiveQuery subscriptions from
|
|
201
|
+
`Parse::Query#subscribe`, `Model.subscribe`, and `client.subscribe(query)`.
|
|
202
|
+
Equality and `$in` used to return no rows and `$ne` every row, while direct
|
|
203
|
+
and aggregate reads already matched. The new
|
|
204
|
+
`Parse::Query#compile_rest_where` returns the where clause in that form.
|
|
205
|
+
|
|
206
|
+
#### Search
|
|
207
|
+
|
|
208
|
+
- **FIXED**: Atlas Search (`Parse::AtlasSearch.search`, `search_with_stage`,
|
|
209
|
+
`autocomplete`, `Parse::Query#atlas_search`) and native hybrid search apply
|
|
210
|
+
the CLP `pointerFields` / `readUserFields` ownership constraint before the
|
|
211
|
+
page is cut. For native hybrid, the lexical input filters before its
|
|
212
|
+
per-input `$limit`, and the fused result is filtered again before the final
|
|
213
|
+
`$limit`. Rows used to be filtered after the page was cut, so when the
|
|
214
|
+
top-ranked hits belonged to other users a scoped caller got a short or empty
|
|
215
|
+
page. Inaccessible rows are still never returned.
|
|
216
|
+
- **IMPROVED**: `Parse::VectorSearch.search` and native hybrid search enforce
|
|
217
|
+
the `pointerFields` ownership constraint server-side across the whole
|
|
218
|
+
candidate window. When every owner field is declared as a scalar pointer
|
|
219
|
+
(`belongs_to`) and its storage path (`_p_<field>`) is a `type: "filter"`
|
|
220
|
+
field of the vectorSearch index, the constraint also goes
|
|
221
|
+
into `$vectorSearch.filter`, so other users' rows no longer use up the
|
|
222
|
+
candidate window. The prefilter is used only while the index is READY and
|
|
223
|
+
serving its latest definition. If Atlas refuses it (for example mid-rebuild),
|
|
224
|
+
the search reruns once without it and skips it for the index cache TTL. A
|
|
225
|
+
failed index lookup is also remembered for that TTL.
|
|
226
|
+
|
|
227
|
+
- **FIXED**: A `$vectorSearch` refusal caused by the caller's own
|
|
228
|
+
`vector_filter` on an unindexed path no longer switches off the owner
|
|
229
|
+
prefilter for every caller on that index or clears the index cache; only a
|
|
230
|
+
refusal naming an owner `_p_<field>` path does. Native hybrid search uses
|
|
231
|
+
`Parse::VectorSearch.default_index` when the vector branch names no index.
|
|
232
|
+
|
|
233
|
+
#### Documentation
|
|
234
|
+
|
|
235
|
+
- **FIXED**: README and query YARD examples use `:field.pointer_id` and
|
|
236
|
+
`:field.array_size`, the names the DSL has used since 5.8.0.
|
|
237
|
+
|
|
238
|
+
#### Behavior Notes
|
|
239
|
+
|
|
240
|
+
- An OR that includes an unconstrained branch now returns every row. The one
|
|
241
|
+
exception is an empty receiver of `|`, which starts the OR. Code that passed
|
|
242
|
+
an empty query for "no filter" should leave that branch out. `:or => []`
|
|
243
|
+
matches no rows.
|
|
244
|
+
- A pipeline-only constraint inside an OR now raises `ArgumentError`. Apply it
|
|
245
|
+
outside the OR, or run the branches as separate queries.
|
|
246
|
+
- Combining queries that run under different authorities or against
|
|
247
|
+
different Parse applications now raises `ArgumentError`. That covers OR
|
|
248
|
+
composition, `:or` Hash branches, and subqueries, and includes different
|
|
249
|
+
session tokens, a session against `scope_to_user` / `scope_to_role` / the
|
|
250
|
+
master key, and different scoped users or roles. Setting a new session
|
|
251
|
+
token, master-key flag, scope, or client on a query that took its authority
|
|
252
|
+
from another query also raises, as does passing different explicit auth to
|
|
253
|
+
a direct or Atlas terminal. Query-shaping options (`limit`, `order`, `keys`,
|
|
254
|
+
`skip`, `include`) inside an `:or` branch raise.
|
|
255
|
+
- LiveQuery subscriptions, push targeting, and subqueries with pipeline-only
|
|
256
|
+
constraints raise `ArgumentError`. Subscribing to a `scope_to_user` /
|
|
257
|
+
`scope_to_role` query, or passing a `session_token:` that differs from the
|
|
258
|
+
query's session, raises.
|
|
259
|
+
- REST queries, LiveQuery subscriptions, and push targeting that compare a
|
|
260
|
+
bare objectId to a declared pointer field now return the matching rows.
|
|
261
|
+
Undeclared fields are unchanged. If a model declares `belongs_to` for a
|
|
262
|
+
field whose server column holds a plain String, a bare string compared to
|
|
263
|
+
that field is now sent as a Pointer and stops matching those values;
|
|
264
|
+
declare such a field as a `:string` property instead.
|
|
265
|
+
- Code that relied on `master: true` keeping master authority inside
|
|
266
|
+
`Parse.without_master_key` should wrap that call in `Parse.with_master_key`.
|
|
267
|
+
A dropped `master: true` raises `ACLRequired` under either
|
|
268
|
+
`Parse::ACLScope.require_session_token` or
|
|
269
|
+
`Parse::AtlasSearch.require_session_token`, with a message naming the block,
|
|
270
|
+
and no longer emits the no-ACL banner. The role-graph helpers
|
|
271
|
+
(`users_in_role_subtree` and related) raise `ACLRequired` for
|
|
272
|
+
`master: true` inside the block; pass `as:` instead.
|
|
273
|
+
- A LiveQuery connection that already connected with the master key outside
|
|
274
|
+
`Parse.without_master_key` stays elevated for subscriptions made inside the
|
|
275
|
+
block, because Parse Server authorizes per connection; the SDK warns.
|
|
276
|
+
- `Parse.without_master_key` guards against accidental master-key use and is
|
|
277
|
+
not an isolation boundary.
|
|
278
|
+
- `Parse::Role.all_for_user` and `Parse::Role#all_parent_role_names` without
|
|
279
|
+
`master:` or `as:` return the full role closure inside
|
|
280
|
+
`Parse.without_master_key`, because the role graph is read as SDK metadata.
|
|
281
|
+
- `Parse::Session._preload_identity_for_destroy!` and the
|
|
282
|
+
`_after_batch_destroy` hooks are now private.
|
|
283
|
+
- A session deleted elsewhere whose owner this process never recorded keeps
|
|
284
|
+
resolving until the next rate-limited reset or `identity_cache_ttl`.
|
|
285
|
+
- A custom identity plane gets the cross-process revocation-race guarantee by
|
|
286
|
+
implementing `invalidation_nonce` and `bump_invalidation_nonce`, as the
|
|
287
|
+
built-in Redis plane does. One with generation counters only is covered for
|
|
288
|
+
single-token and per-user invalidations, but a lookup racing another
|
|
289
|
+
process's full reset is bounded by `identity_cache_ttl` when the plane's
|
|
290
|
+
`clear` also deletes its counters.
|
|
291
|
+
- A sub-agent's `tools:` or `methods:` `only:` list that leaves nothing once
|
|
292
|
+
the parent's `only:` and `except:` lists apply now raises `ArgumentError`.
|
|
293
|
+
An explicitly empty `only: []` is accepted. A sub-agent passing
|
|
294
|
+
`master_atlas: true` under a parent without it raises `ArgumentError`.
|
|
295
|
+
- Parse Server sends a `before_save` webhook only the resulting value of a
|
|
296
|
+
dotted operator. When the handler changes the record, the SDK writes back
|
|
297
|
+
changed sub-keys one level deep and deletes removed ones by path. Two
|
|
298
|
+
concurrent writes inside the same sub-key (including deeper paths under it)
|
|
299
|
+
can still overwrite each other, an array is written whole, and a changed
|
|
300
|
+
sub-key carrying a typed value (Date, Bytes, Pointer) makes the whole field
|
|
301
|
+
be written. A client's whole replace of a sub-document becomes a merge. The
|
|
302
|
+
save response reports the dotted keys; the JavaScript SDK applies them, and
|
|
303
|
+
other clients may need a fetch. A handler operator that cannot be folded,
|
|
304
|
+
or two conflicting dotted keys from the handler, fail the save with an
|
|
305
|
+
error.
|
|
306
|
+
- A `before_save` handler that returns `true`, `nil`, or a Hash keeps the
|
|
307
|
+
client's write as sent, so a client create without an ACL through such a
|
|
308
|
+
handler is stored public read and write, as before, unless the handler
|
|
309
|
+
assigns an ACL on `parse_object`.
|
|
310
|
+
- Saving a relation collection on its own sends removals and additions as two
|
|
311
|
+
requests. If the second fails, the first stays applied and the unsent
|
|
312
|
+
operations stay staged.
|
|
313
|
+
- Owner fields stored as arrays of pointers, undeclared, or not filter-indexed
|
|
314
|
+
get no `$vectorSearch` prefilter. Vector search then fills results only from
|
|
315
|
+
its existing candidate window.
|
|
316
|
+
- A custom identity cache with neither `invalidate_value` nor generation
|
|
317
|
+
support (`generation`, `generation_current?`, `bump_generation`) only
|
|
318
|
+
expires a user's tokens at `identity_cache_ttl` after a password change,
|
|
319
|
+
account deletion, `logout_all!`, or a batch destroy of sessions.
|
|
320
|
+
|
|
321
|
+
### 5.8.0
|
|
322
|
+
|
|
323
|
+
#### Breaking Changes
|
|
324
|
+
|
|
325
|
+
- **BREAKING**: `query_class`, `count_objects`, `export_data`,
|
|
326
|
+
`explain_query`, and `atlas_text_search` refuse a caller `where:`,
|
|
327
|
+
`order:`, or `filter:` (including inside `$inQuery`/`$select` subqueries)
|
|
328
|
+
on a field outside the class's `agent_fields`, with `AccessDenied`
|
|
329
|
+
(`kind: :field_denied`). Before 5.8 these calls ran and let an agent infer a
|
|
330
|
+
hidden field's value from which rows matched or how they were ordered.
|
|
331
|
+
**Migration:** add the field to `agent_fields` if agents should filter or
|
|
332
|
+
sort on it, or drop the constraint. Classes without `agent_fields` are
|
|
333
|
+
unaffected.
|
|
334
|
+
- **BREAKING**: Session termination (`DELETE` with `Mcp-Session-Id`) passes the
|
|
335
|
+
Origin policy, authenticates through the agent factory (401 when refused),
|
|
336
|
+
and is refused with 403 for a session owned by another principal.
|
|
337
|
+
**Migration:** send the same credentials on `DELETE` as on the session's
|
|
338
|
+
other requests.
|
|
339
|
+
- **BREAKING**: An MCP request whose `Mcp-Session-Id` is bound to another
|
|
340
|
+
principal is refused with 403 (`notifications/cancelled` and elicitation
|
|
341
|
+
replies are instead ignored with a silent 202, so they reveal nothing).
|
|
342
|
+
`resources/subscribe` needs a session the caller established through
|
|
343
|
+
`initialize` or its listening stream; an unknown session id gets 404, the
|
|
344
|
+
MCP signal to re-initialize. Previously a caller who knew a session id
|
|
345
|
+
could unsubscribe its resources, fill its subscription cap, or route an
|
|
346
|
+
approval prompt to its stream, and any signed-in caller could invent
|
|
347
|
+
session ids to fill the global subscription limit. **Migration:** clients
|
|
348
|
+
that follow the MCP lifecycle need no change; send the same credentials on
|
|
349
|
+
every request of a session and re-initialize on 404.
|
|
350
|
+
- **BREAKING**: Atlas Search and vector search refuse a session-, user-, or
|
|
351
|
+
role-scoped query that lets a CLP protected field decide which rows match,
|
|
352
|
+
how they rank, or how they are filtered. This covers `fields:`, builder
|
|
353
|
+
block and `search_with_stage` paths (nested operators, wildcard and `multi`
|
|
354
|
+
path objects, `queryString`), highlight and autocomplete fields, `sort:`
|
|
355
|
+
keys, filter keys (nested, dotted, and `_p_` forms), vector fields and
|
|
356
|
+
filters, the native `$rankFusion` path, and faceted search, and a search
|
|
357
|
+
that names no fields while the scope has protected fields. Before, the
|
|
358
|
+
field was only stripped from results, so its value could be probed through
|
|
359
|
+
matching. Master scopes and classes with nothing protected are unaffected.
|
|
360
|
+
**Migration:** pass `fields:` naming the fields to search, and drop
|
|
361
|
+
protected fields from filters, sorts, and highlights.
|
|
362
|
+
- **BREAKING**: Several constraints on the same field now all apply.
|
|
363
|
+
`query(:plays => 5, :plays.gt => 1)` compiles to
|
|
364
|
+
`{"plays" => {"$eq" => 5, "$gt" => 1}}` instead of keeping only the last
|
|
365
|
+
one; conflicting constraints (the same operator twice with different
|
|
366
|
+
values, two equalities, two regexes) are combined in a top-level `$and`.
|
|
367
|
+
**Migration:** code that relied on a later `where` replacing an earlier
|
|
368
|
+
constraint on the same field should clear it with `query.clear(:where)` or
|
|
369
|
+
build a fresh query.
|
|
370
|
+
- **BREAKING**: `limit(0)` (and a negative limit) returns no rows without a
|
|
371
|
+
request, and aggregate helpers on such a query return their empty result.
|
|
372
|
+
Before, the limit was omitted and the server default of 100 rows came back.
|
|
373
|
+
**Migration:** use `limit(nil)` to clear a limit.
|
|
374
|
+
- **BREAKING**: `sum`, `average`, `min`, `max`, `count_distinct`, and
|
|
375
|
+
`group_by` apply the query's `order`, `skip`, and `limit` before grouping,
|
|
376
|
+
so `query.skip(10).sum(:plays)` sums the rows after the first ten instead of
|
|
377
|
+
returning nil, and `order(:plays.desc).limit(2).average(:plays)` averages
|
|
378
|
+
the top two rows. `distinct` and `group_by_date` are unchanged.
|
|
379
|
+
**Migration:** sort or trim grouped output in Ruby, or use `group_by`'s own
|
|
380
|
+
`.order(...)`.
|
|
381
|
+
- **BREAKING**: The query DSL no longer defines `Symbol#id` or `Symbol#size`.
|
|
382
|
+
`Symbol#id` made every symbol look like a record to ActiveRecord, so
|
|
383
|
+
`where(status: :draft)` raised on Rails 8.1 (#83), and `Symbol#size`
|
|
384
|
+
replaced Ruby's own method, so `sort_by(&:size)` raised. **Migration:**
|
|
385
|
+
write `:tags.array_size => 2` for `:tags.size => 2` and
|
|
386
|
+
`:author.pointer_id => id` for `:author.id => id`.
|
|
387
|
+
- **BREAKING**: Assigning a value a property cannot represent raises
|
|
388
|
+
`Parse::Properties::TypecastError` (an `ArgumentError`) instead of storing
|
|
389
|
+
a guess. Previously `true` on an `:integer` became a Delete that erased the
|
|
390
|
+
column, `""` became `0`, `"1.5"` became `1`, and NaN or Infinity were sent
|
|
391
|
+
as null. `:string` refuses Arrays and Hashes and stores `false` as
|
|
392
|
+
`"false"`, `:date` refuses Numerics, `:boolean` reads `"no"` and `"n"` as
|
|
393
|
+
false, and `Parse::GeoPoint` raises on non-numeric or non-finite
|
|
394
|
+
coordinates instead of becoming (0, 0). Blank strings clear the field, and
|
|
395
|
+
data loaded from the server never raises. **Migration:** convert untrusted
|
|
396
|
+
input before assigning it (for example `Integer(params[:n], exception:
|
|
397
|
+
false)`, `Time.at(epoch)`), or rescue `Parse::Properties::TypecastError`
|
|
398
|
+
where form input is assigned directly.
|
|
399
|
+
- **BREAKING**: Unsaved objects (no objectId) compare equal only to
|
|
400
|
+
themselves and hash by identity, so `uniq`, `Set`, and Hash keys no longer
|
|
401
|
+
merge distinct new records. **Migration:** an object's hash changes when it
|
|
402
|
+
is saved; rebuild a Set or Hash keyed by unsaved objects after saving.
|
|
403
|
+
- **BREAKING**: `destroy` keeps the objectId and sets `destroyed?`;
|
|
404
|
+
`persisted?` is then false, and `save` returns false (`save!` raises
|
|
405
|
+
`Parse::RecordNotSaved`) instead of recreating the record. **Migration:**
|
|
406
|
+
check `destroyed?` rather than `id.nil?` after `destroy`.
|
|
407
|
+
- **BREAKING**: Saving a record that references an unsaved object in a
|
|
408
|
+
pointer, array, or relation field fails instead of storing a pointer with a
|
|
409
|
+
null objectId. Single saves return false with an error on the field; batch
|
|
410
|
+
and transaction saves raise `Parse::RecordNotSaved`. **Migration:** save the
|
|
411
|
+
referenced object first, or in a `before_save` callback.
|
|
412
|
+
- **BREAKING**: An object built from a server row with no `ACL` key has a
|
|
413
|
+
nil `acl` (the row is public on the server) instead of the class default
|
|
414
|
+
ACL. **Migration:** handle a nil `acl` on such rows.
|
|
415
|
+
- **BREAKING**: The `as:` owner option is honored only as a Symbol key and
|
|
416
|
+
only as a `Parse::User`, a pointer to `_User`, or a user objectId String. A
|
|
417
|
+
String `"as"` key (as in form or JSON params) is dropped, and `"*"`,
|
|
418
|
+
`role:` keys, and other objects raise `ArgumentError`, so request input can
|
|
419
|
+
no longer choose a record's ACL owner or make it public. **Migration:** pass
|
|
420
|
+
the owner as `as: user` from server code.
|
|
421
|
+
- **BREAKING**: `Array#save` and `Array#destroy` raise `ArgumentError` on a
|
|
422
|
+
non-empty array that contains no Parse objects, instead of doing nothing and
|
|
423
|
+
returning a truthy `BatchOperation`. When one chunk of a batch raises, the
|
|
424
|
+
chunks that succeeded are still applied before the exception is re-raised,
|
|
425
|
+
and `responses` holds one entry per request. **Migration:** call them only
|
|
426
|
+
on arrays of `Parse::Object`, and read per-request responses.
|
|
427
|
+
|
|
428
|
+
- **BREAKING**: Mongo-direct reads (`results_direct`, `count_direct`,
|
|
429
|
+
`first_direct`, `distinct_direct`, and queries that auto-route there) no
|
|
430
|
+
longer fall back to the master key for non-master clients. A client from
|
|
431
|
+
`Parse::Client#become`, `Parse::User#session_client`, or a webhook
|
|
432
|
+
`user_client` is scoped to its session, and a client without a master key,
|
|
433
|
+
`Parse.client_mode`, or `use_master_key = false` reads in the public scope,
|
|
434
|
+
as REST does. **Migration:** set `use_master_key = true` on a master-keyed
|
|
435
|
+
client, or pass `master: true`, where a master read is intended.
|
|
436
|
+
- **BREAKING**: Scoped mongo-direct queries refuse to filter, sort, or join
|
|
437
|
+
on a protectedFields column, or to copy one through `$$ROOT`,
|
|
438
|
+
`$$CURRENT`, `$getField`, or a join sub-pipeline, with
|
|
439
|
+
`Parse::CLPScope::Denied`, as REST refuses with error 119. They also apply
|
|
440
|
+
Parse Server's default `_User` protection (other users' `email`) and each
|
|
441
|
+
included or joined class's protected fields, and a `$graphLookup` into a
|
|
442
|
+
class with protected fields is refused. **Migration:** run such queries
|
|
443
|
+
with `master: true`, and set `Parse::CLPScope.default_protected_fields` if
|
|
444
|
+
your server's `protectedFields` option differs from the default.
|
|
445
|
+
- **BREAKING**: `Parse.with_session(nil)`, and `with_session` given a user or
|
|
446
|
+
session with no token, run the block anonymously (no session token and no
|
|
447
|
+
master key) instead of with the master key. **Migration:** pass
|
|
448
|
+
`use_master_key: true` on calls that need it; `Parse.anonymous_session?`
|
|
449
|
+
reports the state.
|
|
450
|
+
- **BREAKING**: Signup (`Parse::User#signup!`, `.create`, `.signup`,
|
|
451
|
+
`.anonymous_signup`, `.autologin_service`) no longer sends the master key
|
|
452
|
+
or an ambient session, so Parse Server returns a session token and
|
|
453
|
+
`upgrade_anonymous!` works. **Migration:** deployments that close `_User`
|
|
454
|
+
create and provision users server-side should pass `use_master_key: true`.
|
|
455
|
+
- **BREAKING**: A `before_save` webhook that makes no change keeps the
|
|
456
|
+
client's write as sent (it used to erase the whole write), and one that
|
|
457
|
+
does change the object replies with the client's full write plus its
|
|
458
|
+
changes, so operators, undeclared fields, and signup fields survive. An
|
|
459
|
+
`after_find` webhook can no longer rewrite results (Parse Server blanks
|
|
460
|
+
them and crashed on `nil`); it keeps the rows or denies the find. A call to
|
|
461
|
+
an unregistered function returns an error, and replay dedup runs only with
|
|
462
|
+
a request id, a nonce header, or a signature. **Migration:** remove
|
|
463
|
+
undeclared fields explicitly in `before_save`, move row filtering to
|
|
464
|
+
`before_find`, ACLs, or CLPs, and register every function Parse Server
|
|
465
|
+
calls.
|
|
466
|
+
- **BREAKING**: `add!`, `add_unique!`, and `remove!` on collections return
|
|
467
|
+
`true` or `false`, and an array `has_many` or `belongs_to` refuses `nil`,
|
|
468
|
+
non-object values, and objects of another class when the declared class is
|
|
469
|
+
a registered model (an objectId String becomes a pointer of the declared
|
|
470
|
+
class). Setting a field on a bare `Parse::Pointer` raises instead of
|
|
471
|
+
writing a hidden copy that was never saved. **Migration:** check the
|
|
472
|
+
boolean, pass objects of the declared class, and fetch a pointer before
|
|
473
|
+
editing it.
|
|
474
|
+
|
|
475
|
+
#### MCP deployments can expose less than their users can read
|
|
476
|
+
|
|
477
|
+
- **NEW**: `Parse::Agent.new(fields: { Customer => %i[display_name timezone], default: [...] })`
|
|
478
|
+
narrows a class's `agent_fields` ceiling for one agent, so two MCP
|
|
479
|
+
deployments in one process can expose different subsets of the same model.
|
|
480
|
+
A policy can never widen past `agent_fields`, a class without `agent_fields`
|
|
481
|
+
is narrowed to exactly the listed fields, and a sub-agent intersects its
|
|
482
|
+
parent's policy. The effective set applies everywhere the class allowlist
|
|
483
|
+
did: projection, `keys:`, include projections, aggregation pipelines, Atlas
|
|
484
|
+
Search fields, `get_schema`, `completion/complete`, exports,
|
|
485
|
+
`agent.describe`, and `semantic_search` chunk text, reranker input, and
|
|
486
|
+
filter fields. The policy lives in fiber storage for the duration of each
|
|
487
|
+
tool call, so concurrent agents never see each other's policy, and threads
|
|
488
|
+
or fibers a custom tool starts inherit it.
|
|
489
|
+
- **FIXED**: `query_class`, `count_objects`, and `export_data` accepted a
|
|
490
|
+
caller `where:` or `order:` on a field outside `agent_fields`, so an agent
|
|
491
|
+
could infer a hidden field's value from which rows matched or how they were
|
|
492
|
+
ordered. They now refuse with `AccessDenied` (`kind: :field_denied`), as
|
|
493
|
+
`group_by`, `distinct`, and aggregation already did. Keys are resolved the
|
|
494
|
+
way queries send them, so `play_count` is checked as `playCount` and a
|
|
495
|
+
`_p_` prefix is ignored. Server-owned tenant, per-agent, and canonical
|
|
496
|
+
filters are not affected. A `fields:` String key such as `"User"` resolves
|
|
497
|
+
to the class's Parse name (`_User`), and a tool that invokes another agent
|
|
498
|
+
runs under both agents' policies.
|
|
499
|
+
- **FIXED**: `atlas_text_search` accepted a `filter:` on a field outside the
|
|
500
|
+
allowlist, and without `fields:` it (and a non-empty faceted search query)
|
|
501
|
+
searched every field; `explain_query` never checked its `where:`. Each could
|
|
502
|
+
reveal a hidden field's value. Filters are now checked, text search defaults
|
|
503
|
+
to the readable fields when an allowlist applies (and is refused when none
|
|
504
|
+
is readable, rather than falling back to a wildcard), hybrid retrieval
|
|
505
|
+
profiles restrict their lexical branch to readable fields, and
|
|
506
|
+
`explain_query` refuses a hidden field in `where:`.
|
|
507
|
+
- **FIXED**: Subqueries could reach fields a direct query would be refused:
|
|
508
|
+
`$inQuery`, `$notInQuery`, `$select`, and `$dontSelect` predicates (and a
|
|
509
|
+
`$select` key) are now checked against their target class's effective
|
|
510
|
+
allowlist, so an equivalent subquery is no longer an oracle for a hidden
|
|
511
|
+
field.
|
|
512
|
+
- **FIXED**: `call_method` results projected only the top-level object, so a
|
|
513
|
+
returned object's embedded children (or object JSON inside a returned Hash,
|
|
514
|
+
saved or unsaved) could carry fields outside their own class's allowlist.
|
|
515
|
+
Every embedded object is now projected through its own class's effective
|
|
516
|
+
allowlist.
|
|
517
|
+
- **FIXED**: Field refusals from `group_by`, `group_by_date`, and `distinct`
|
|
518
|
+
passed the refusal into `AccessDenied`'s class-name slot, so the message was
|
|
519
|
+
a stringified Hash and `kind`, `denied_field`, and `allowed_fields` were
|
|
520
|
+
lost. They now carry the structured details like every other refusal.
|
|
521
|
+
|
|
522
|
+
#### Supported user-scoped and analytics deployment patterns
|
|
523
|
+
|
|
524
|
+
- **FIXED**: A `$relatedTo` constraint's `key` was not checked against the
|
|
525
|
+
owning class's field policy, so `count_objects` on `_User` with
|
|
526
|
+
`$relatedTo: { object: Post#X, key: "flaggedBy" }` revealed a relation
|
|
527
|
+
hidden from `Post`'s allowlist. The key is now checked like `$select` keys,
|
|
528
|
+
with the owner resolved from a pointer hash, a `Parse::Pointer`, or a
|
|
529
|
+
`"Class$id"` string.
|
|
530
|
+
- **FIXED**: Agent tools checked a `where:`, `order:`, or filter key under one
|
|
531
|
+
field name and queried another. A declared column such as `PublicText`
|
|
532
|
+
passed the check and was sent as `publicText`, and two properties whose
|
|
533
|
+
Ruby and remote names crossed could be checked under one and queried under
|
|
534
|
+
the other. Validation and execution now share one name rule (a Ruby
|
|
535
|
+
property name maps to its declared column first, then an exact declared
|
|
536
|
+
column is kept), and `order:` is sent under the checked names.
|
|
537
|
+
- **FIXED**: Aggregation through agent tools could still read hidden fields:
|
|
538
|
+
a pipeline that never projects (`[{ "$limit" => 1 }]`) returned whole
|
|
539
|
+
documents, `$$ROOT.secret` and `$$CURRENT` references were not checked, and
|
|
540
|
+
`$lookup` / `$graphLookup` join keys (`localField`, `foreignField`, `let`,
|
|
541
|
+
`startWith`, `connectFromField`, `connectToField`,
|
|
542
|
+
`restrictSearchWithMatch`) were not checked. Rows whose source document
|
|
543
|
+
reaches the output are now projected to the allowlist, and every one of
|
|
544
|
+
those references is checked against the right class.
|
|
545
|
+
- **FIXED**: An `agent_method` returning aggregation rows
|
|
546
|
+
(`Parse::AggregationResult`) passed Parse Server's internal columns
|
|
547
|
+
(`_rperm`, `_hashed_password`, `_auth_data_*`) through to the caller. They
|
|
548
|
+
are now removed at every depth, as on every other aggregation path.
|
|
549
|
+
- **IMPROVED**: Each principal may hold at most 100 session bindings (the
|
|
550
|
+
shared master-key principal of an endpoint without a `principal_resolver`
|
|
551
|
+
is bounded only by the registry). Past that its own least recently used
|
|
552
|
+
idle binding is
|
|
553
|
+
evicted, so one caller flooding `initialize` can no longer push other
|
|
554
|
+
principals' idle sessions out of the registry and then claim their ids.
|
|
555
|
+
`initialize` and `resources/subscribe` are charged against the principal's
|
|
556
|
+
rate limiter (429 when exhausted). A skipped live session moves to the back
|
|
557
|
+
of the eviction order, so a registry full of live sessions no longer makes
|
|
558
|
+
every new binding rescan them under the lock. An owner's ordinary requests
|
|
559
|
+
refresh its session's position, sessions holding resource subscriptions are
|
|
560
|
+
never evicted, and a listening stream is rate-charged and capacity-checked
|
|
561
|
+
before it claims a session id.
|
|
562
|
+
- **IMPROVED**: A listening stream's revalidation thread is woken on close
|
|
563
|
+
rather than killed, so it can no longer be interrupted inside a REST call
|
|
564
|
+
and return a pooled connection mid-response. A revalidator that reports the
|
|
565
|
+
session invalid closes the stream at once; one that raises is retried, and
|
|
566
|
+
the third consecutive raise closes it. `user_scoped` now separates a
|
|
567
|
+
rejected session from a Parse Server outage: an outage (unreachable server,
|
|
568
|
+
5xx) fails the request with 401 but keeps the token cached and counts as a
|
|
569
|
+
transient stream error, instead of closing every open stream and evicting
|
|
570
|
+
the token.
|
|
571
|
+
- **FIXED**: A listening-stream body closed before Rack iterated it detached
|
|
572
|
+
the session's active stream, and a close that raced the attach left a
|
|
573
|
+
listener registered with no stream. A body now detaches only a listener it
|
|
574
|
+
attached.
|
|
575
|
+
|
|
576
|
+
- **NEW**: `Parse::Agent::MCPRackApp.user_scoped(...)` builds an endpoint for
|
|
577
|
+
signed-in application users. The session token comes from
|
|
578
|
+
`Authorization: Bearer` or `X-Parse-Session-Token` (or a custom
|
|
579
|
+
`session_token_from:`); a missing, blank, or invalid token gets 401 before
|
|
580
|
+
any agent is built, and it never falls back to the master key. Identity and
|
|
581
|
+
tenant are pinned server-side (`tenant_from:`), and `agent_options:` passes
|
|
582
|
+
extra agent settings such as `fields:` while refusing identity or authority
|
|
583
|
+
keys (and `master_atlas` and `allow_mutations`). `permissions:` accepts
|
|
584
|
+
`:readonly` (default) or `:write`; `:admin` is refused because it skips the
|
|
585
|
+
spend cap and score quantization for every signed-in user. Sessions are
|
|
586
|
+
owned by the verified user id, so a refreshed token keeps its session.
|
|
587
|
+
`session_validation:` (`:per_request` or `:cached`) and
|
|
588
|
+
`session_revalidate_interval:` (default 60s) control revocation, and
|
|
589
|
+
`MCPRackApp.new` accepts the underlying `listening_stream_revalidator:` and
|
|
590
|
+
`listening_stream_revalidate_interval:`.
|
|
591
|
+
- **NEW**: `Parse::Agent::MCPRackApp.master_analytics(principal_resolver:, ...)`
|
|
592
|
+
builds a shared master-key endpoint (read-only by default) that refuses to
|
|
593
|
+
construct without a callable `principal_resolver`: without one every
|
|
594
|
+
master-key caller fingerprints as the same principal and shares stream,
|
|
595
|
+
approval, and cancellation ownership. An unresolved operator gets 401.
|
|
596
|
+
Direct `MCPRackApp.new` construction and single-operator master-key use are
|
|
597
|
+
unchanged.
|
|
598
|
+
- **IMPROVED**: The owner binding of a live session (an attached listening
|
|
599
|
+
stream or a pending approval prompt) is never evicted under LRU pressure,
|
|
600
|
+
so flooding new sessions cannot strip a victim's owner. `user_scoped`
|
|
601
|
+
refuses `master_atlas` and `allow_mutations` in `agent_options`.
|
|
602
|
+
- **FIXED**: Session termination ran before authentication, so any caller who
|
|
603
|
+
knew a session id could cancel its requests and drop its approvals,
|
|
604
|
+
subscriptions, and log level (see Breaking Changes).
|
|
605
|
+
- **IMPROVED**: When the session registry is full of live sessions, a new
|
|
606
|
+
session or stream is refused with 503 rather than admitted without an
|
|
607
|
+
owner.
|
|
608
|
+
- **FIXED**: `notifications/cancelled` and elicitation replies were bound only
|
|
609
|
+
to the session id, so a caller who knew another principal's
|
|
610
|
+
`Mcp-Session-Id` could cancel its requests or answer its approval prompts.
|
|
611
|
+
A session bound to an owner now accepts them only from that owner; a
|
|
612
|
+
mismatch is a silent 202.
|
|
613
|
+
- **FIXED**: `Parse::Authorization` resolved a session through `/users/me`
|
|
614
|
+
without bypassing the response cache, so a revoked token could re-resolve
|
|
615
|
+
from a cached response. Revocation is now bounded by the identity cache's
|
|
616
|
+
TTL and invalidation hooks. The guide documents the interval for each path
|
|
617
|
+
(REST, mongo-direct, listening streams).
|
|
618
|
+
- **IMPROVED**: The deployment factories keep one rate limiter per
|
|
619
|
+
principal (the session's user, or the resolved operator), bounded to the
|
|
620
|
+
most recently seen 10,000, so `rate_limit:` accumulates across requests
|
|
621
|
+
instead of resetting with each request's fresh agent. An injected
|
|
622
|
+
`rate_limiter:` in `agent_options` is used as-is.
|
|
623
|
+
- **NEW**: Orphaned subscription sessions (subscribed but never streamed, or
|
|
624
|
+
whose stream closed without `DELETE`) become eligible for reaping after
|
|
625
|
+
`orphan_ttl` (default 300 seconds, `nil` disables), releasing their
|
|
626
|
+
LiveQuery subscriptions. Reaping runs on the next `subscribe` or stream
|
|
627
|
+
attach; on an otherwise idle server, call
|
|
628
|
+
`MCPSubscriptions::Manager#reap_orphans!` on a timer.
|
|
629
|
+
- **FIXED**: A client that reconnected a listening stream before the old one
|
|
630
|
+
closed lost its subscriptions: the old stream's close detached the session,
|
|
631
|
+
unregistering the new stream's delivery and tearing down its LiveQuery
|
|
632
|
+
subscriptions. A superseded stream now detaches nothing; the latest stream
|
|
633
|
+
owns the session, and `DELETE` still tears it down.
|
|
634
|
+
- **FIXED**: A listening stream that closed while its first frame was being
|
|
635
|
+
written could start its heartbeat and identity-revalidation threads after
|
|
636
|
+
closing, leaving them running indefinitely. Both start under the close lock
|
|
637
|
+
and never once the stream is closed.
|
|
638
|
+
|
|
639
|
+
#### Opt-in server field names in returned data
|
|
640
|
+
|
|
641
|
+
- **NEW**: `Parse::Agent.new(field_names: :server)` and
|
|
642
|
+
`aggregation.results(field_names: :server)` return data keyed by the exact
|
|
643
|
+
server field names (`createdAt`, `totalPlays`, an explicit `field_map`
|
|
644
|
+
alias such as `ExternalID`) as Strings. Omitting the option keeps every
|
|
645
|
+
API's current behavior. Most agent output already used server names; the
|
|
646
|
+
option matters for `AggregationResult#to_h` and `#keys`, which default to
|
|
647
|
+
snake_case Symbols, and keeps distinct keys such as `totalPlays` and
|
|
648
|
+
`total_plays` instead of collapsing them. In server mode a snake_case
|
|
649
|
+
method name resolves when it matches exactly one key and raises naming the
|
|
650
|
+
candidates when it matches several. The mode is scoped per tool call, is
|
|
651
|
+
inherited by sub-agents, and never changes field restrictions: access
|
|
652
|
+
policies apply identically in either mode. Unsupported values raise
|
|
653
|
+
`ArgumentError`. It does not enable the REST aggregate `raw_field_names:`
|
|
654
|
+
or `raw_values:` flags.
|
|
655
|
+
- **FIXED**: `call_method` serialized a returned Parse object from its
|
|
656
|
+
field-type map, emitting `{"title" => "string"}` instead of values; it now
|
|
657
|
+
serializes the object's data (still projected and redacted). A returned
|
|
658
|
+
`AggregationResult` was emitted as its `inspect` string; it is now a hash.
|
|
659
|
+
|
|
660
|
+
#### Retrieval profiles for `semantic_search`
|
|
661
|
+
|
|
662
|
+
- **FIXED**: On a class without `agent_fields`, a hybrid profile's lexical
|
|
663
|
+
branch searched every column (`wildcard: "*"`), so CLP `protectedFields`
|
|
664
|
+
could decide which documents matched. It now searches the embedded text
|
|
665
|
+
sources unless the profile names fields.
|
|
666
|
+
- **FIXED**: `semantic_search` filters could use an `agent_searchable
|
|
667
|
+
filter_fields:` entry outside `agent_fields` unless a per-agent `fields:`
|
|
668
|
+
policy also applied. Filterable fields are now always bounded by the
|
|
669
|
+
readable fields. A profile's configured lexical field that is an exact
|
|
670
|
+
declared server name (`title_exact`) is kept as written instead of being
|
|
671
|
+
recased and refused. The strict profile budget counts chunk metadata and
|
|
672
|
+
per-chunk overhead, so many tiny chunks no longer return several times the
|
|
673
|
+
budget.
|
|
674
|
+
- **CHANGED**: `semantic_search` refuses a `query` longer than 4,000
|
|
675
|
+
characters, and a profile reranker receives at most 2,000 characters of
|
|
676
|
+
it. The query is paired with every candidate document in a rerank call, so
|
|
677
|
+
an unbounded query multiplied the provider cost of every call.
|
|
678
|
+
|
|
679
|
+
- **NEW**: `Parse::Retrieval::Profiles.register(name, ...)` defines
|
|
680
|
+
server-configured strategies (result counts, hybrid search, a reranker
|
|
681
|
+
referenced by `Parse::Retrieval.register_reranker` name, candidate and
|
|
682
|
+
top-n counts, a per-document text cap, a timeout, a failure mode, and a
|
|
683
|
+
response budget). The `semantic_search` tool takes an optional `profile:`;
|
|
684
|
+
without it behavior is unchanged. Profiles are validated at registration,
|
|
685
|
+
an unknown name at call time is refused with the available list, and an
|
|
686
|
+
agent can never supply a provider, endpoint, or credential.
|
|
687
|
+
- **NEW**: Reranking under a profile cuts each document's text before it
|
|
688
|
+
leaves the process, charges estimated tokens to the tenant's `SpendCap`,
|
|
689
|
+
and is bounded by a timeout. On a timeout or provider error,
|
|
690
|
+
`on_rerank_failure: :fallback` keeps the retrieval order and adds
|
|
691
|
+
`rerank_fallback: true` and `rerank_fallback_reason` to the result;
|
|
692
|
+
`:raise` fails the call.
|
|
693
|
+
- **NEW**: Each `semantic_search` call emits one `parse.retrieval.search`
|
|
694
|
+
notification with the profile, counts, rerank stats, and timings, and never
|
|
695
|
+
the query, document text, field values, URLs, or credentials. A failed call
|
|
696
|
+
emits one too, naming only the error class. Rerank token counts are SDK
|
|
697
|
+
estimates, not provider-reported usage.
|
|
698
|
+
- **CHANGED**: The response token budget covers the whole response: each
|
|
699
|
+
returned parent document counts once, alongside chunk text, so a caller
|
|
700
|
+
passing `max_total_tokens:` may get fewer chunks than in 5.7. Under a
|
|
701
|
+
profile the budget is mandatory: the caller can lower it but not raise or
|
|
702
|
+
disable it, and it is never exceeded, even by an oversized first result. A
|
|
703
|
+
caller's `k` can never raise retrieval above the profile's
|
|
704
|
+
`rerank_candidates`, and rerank options set without a `reranker:` are
|
|
705
|
+
refused at registration.
|
|
706
|
+
- **NEW**: `Parse::Retrieval::Benchmark` scores profiles on a labeled case set
|
|
707
|
+
(recall@k, MRR, hit rate, mean and p95 latency, forbidden-id violations,
|
|
708
|
+
estimated tokens), overall and per tag, through the real `semantic_search`
|
|
709
|
+
tool.
|
|
710
|
+
|
|
711
|
+
#### Contextualized embedding batches adapt to provider limits
|
|
712
|
+
|
|
713
|
+
- **IMPROVED**: Contextualized requests are packed by document count, an
|
|
714
|
+
estimated 120k-token budget (bytes divided by 3, deliberately
|
|
715
|
+
conservative), and the 5.7.5 response-size cap, never splitting a
|
|
716
|
+
document. When Voyage rejects a request as too large, it is halved by
|
|
717
|
+
document and retried (bounded), keeping vectors aligned with their inputs.
|
|
718
|
+
Only recognized size errors trigger a split; a single document that is
|
|
719
|
+
still too large raises an error naming its index. This is robust
|
|
720
|
+
adaptation, not exact token counting.
|
|
721
|
+
- **CHANGED**: Voyage 4xx errors now carry the provider's sanitized error
|
|
722
|
+
detail (`BadRequestError#status`, `#detail`) and include it in the message.
|
|
723
|
+
|
|
724
|
+
#### Vector index definitions from the model
|
|
725
|
+
|
|
726
|
+
- **NEW**: `Parse::VectorSearch::IndexDefinition.build(klass)` (also
|
|
727
|
+
`Parse::Schema.vector_index_definition`) derives an Atlas `vectorSearch`
|
|
728
|
+
definition from the `:vector` property, `agent_searchable filter_fields:`,
|
|
729
|
+
and `agent_tenant_scope`, with deterministic output. `preview` and `diff`
|
|
730
|
+
compare it with a live index, and the `vector_search_index` model macro
|
|
731
|
+
registers it with `SearchIndexMigrator`. Applying stays an explicit
|
|
732
|
+
`apply_search_indexes!` call.
|
|
733
|
+
- **NEW**: `property :embedding, :vector, ..., quantization: :scalar` (or
|
|
734
|
+
`:binary`) adds Atlas automatic quantization to the generated index
|
|
735
|
+
definition, cutting index memory roughly 4x or 32x. Stored vectors and the
|
|
736
|
+
write path are unchanged. Drift detection reports a quantization mismatch
|
|
737
|
+
between the declaration and the live index.
|
|
738
|
+
|
|
739
|
+
#### Queries use a property's declared remote name
|
|
740
|
+
|
|
741
|
+
- **FIXED**: A property declared with an explicit remote name, such as
|
|
742
|
+
`property :account_id, :string, field: :account_id` or
|
|
743
|
+
`property :auth_id_sub, :string, field: :authId_sub`, was saved and read
|
|
744
|
+
under that name, but queries camel-cased it anyway: `where(account_id:)`
|
|
745
|
+
compiled to `accountId`, `order(:auth_id_sub.desc)` to `authIdSub`, and even
|
|
746
|
+
the string key `"authId_sub"` became `authIdSub`, so queries against systems
|
|
747
|
+
whose columns use underscores or mixed casing silently matched nothing.
|
|
748
|
+
Queries now send a declared `field:` name exactly as declared, for both the
|
|
749
|
+
Ruby name and the remote name, across `where`, operators, `order`, `keys`,
|
|
750
|
+
`include`, subqueries (each with its own class's names), aggregation
|
|
751
|
+
helpers (`sum`, `average`, `min`, `max`, group-by), and the mongo-direct
|
|
752
|
+
entry points. Only explicit `field:` names that differ from the default
|
|
753
|
+
camelCase are aliased, never an internal `_` column; other names, the
|
|
754
|
+
built-in system fields, and `Parse::Query.field_formatter` (including
|
|
755
|
+
`nil`) behave exactly as before.
|
|
756
|
+
|
|
757
|
+
- **FIXED**: A block passed to `results`, `first`, `results_direct`, and
|
|
758
|
+
other query methods now runs outside the query's field-alias scope.
|
|
759
|
+
Before, a `Pointer#fetch(keys:)`, a partial `fetch!`, or a cursor used
|
|
760
|
+
inside the block formatted another class's keys with the outer model's
|
|
761
|
+
declared `field:` names, sending `account_id` where the other class expects
|
|
762
|
+
`accountId`.
|
|
763
|
+
- **FIXED**: Partial fetches (`Pointer#fetch`, `fetch!`, `fetch_json`),
|
|
764
|
+
`fetched_keys` tracking, and cursor pagination constraints use the fetched
|
|
765
|
+
class's own declared `field:` names, so `keys: [:account_id]` on a model
|
|
766
|
+
that declares `field: :account_id` requests the right column.
|
|
767
|
+
- **IMPROVED**: Field-alias resolution is cached per class and refreshed when
|
|
768
|
+
a model, `parse_class`, or field is declared, and re-entering the scope for
|
|
769
|
+
the same class skips setup. `Parse::Model.find_class` caches lookups for
|
|
770
|
+
class names with no Ruby model, so queries on such classes are no slower
|
|
771
|
+
than in 5.7.
|
|
772
|
+
- **CHANGED**: The query field-alias scope lives in inheritable fiber storage,
|
|
773
|
+
so threads and fibers started while a query compiles see the same names.
|
|
774
|
+
|
|
775
|
+
#### Vector search uses the stored column for multi-word vector properties
|
|
776
|
+
|
|
777
|
+
- **FIXED**: A `:vector` property is saved under its `field_map` name
|
|
778
|
+
(`property :body_embedding, :vector` is stored as `bodyEmbedding`), but
|
|
779
|
+
`find_similar`, hybrid search, index auto-discovery, and drift checks used
|
|
780
|
+
the Ruby name (`body_embedding`) as the vector path, so a multi-word vector
|
|
781
|
+
property searched a path holding no vectors and auto-discovery could not
|
|
782
|
+
find its index. They now use the stored column, as does the new index
|
|
783
|
+
generator. Single-word properties such as `embedding` are unaffected. An
|
|
784
|
+
index created with the Ruby name as its path must be recreated with the
|
|
785
|
+
stored name; drift detection reports the mismatch.
|
|
786
|
+
|
|
787
|
+
#### Records keep their identity, values, and writes
|
|
788
|
+
|
|
789
|
+
- **NEW**: `:number` is a property type for Parse Number columns: integral
|
|
790
|
+
values read back as Integer and fractional values as Float, so `4.75` is no
|
|
791
|
+
longer truncated to `4` and `5` does not become `5.0`. Server `Number`
|
|
792
|
+
columns (`Parse::Schema`, `auto_generate_models!`) use it, and GraphQL
|
|
793
|
+
exposes it as `Float`. `:integer` and `:float` keep their casting.
|
|
794
|
+
- **NEW**: `destroyed?`, `cache_key`, `cache_version`, and
|
|
795
|
+
`cache_key_with_version` (ActiveRecord-compatible, so fragment caches no
|
|
796
|
+
longer collide across classes or never expire), and `attribute_values`,
|
|
797
|
+
the value form of `attributes` (which stays the property type map that
|
|
798
|
+
ActiveModel serialization reads).
|
|
799
|
+
- **FIXED**: Mass assignment (`attributes=`, `apply_attributes!`, ActiveModel
|
|
800
|
+
`assign_attributes`) could change an object's `id`, so a params hash with
|
|
801
|
+
`"id"` redirected the next save to another record, and `assign_attributes`
|
|
802
|
+
skipped the protected-key filter (`session_token`, timestamps). Both are
|
|
803
|
+
closed, and assigning `objectId` no longer triggers an autofetch.
|
|
804
|
+
- **FIXED**: `persisted?` means the record exists on the server, as
|
|
805
|
+
ActiveModel expects. Before, an object with unsaved changes or missing
|
|
806
|
+
timestamps reported false, so a Rails edit form re-rendered after a failed
|
|
807
|
+
validation submitted to create. Reading `updated_at` or `created_at` after
|
|
808
|
+
`create` no longer marks the record dirty.
|
|
809
|
+
- **FIXED**: In-place edits to `:object` hashes (`obj.meta["k"] = 1`) and to
|
|
810
|
+
hashes inside arrays are detected and saved; they were silently dropped.
|
|
811
|
+
`:array` keeps `nil` elements instead of shifting later positions, assigned
|
|
812
|
+
arrays and hashes are copied so the caller's data is not changed through
|
|
813
|
+
the property, array change history records the real previous value, and
|
|
814
|
+
`rollback!` restores arrays.
|
|
815
|
+
- **FIXED**: Dates nested inside `:array` and `:object` values are saved as
|
|
816
|
+
Parse Dates rather than ISO strings. `Parse::GeoPoint.new(lat:, lng:)` (and
|
|
817
|
+
`latitude:`/`longitude:`, hashes, numeric strings) no longer becomes (0, 0).
|
|
818
|
+
Without phonelib, a US number entered without `+` was saved with a Swiss
|
|
819
|
+
country code; 10-digit North American numbers now get `+1`, results match
|
|
820
|
+
phonelib, and `Parse::Phone.default_country_code` (default `"1"`) controls
|
|
821
|
+
national numbers. `Parse::Bytes` encodes without newlines and
|
|
822
|
+
`attributes=` works, and a `Parse::File` from server data no longer guesses
|
|
823
|
+
`image/jpeg`.
|
|
824
|
+
- **FIXED**: `<field>_increment!` and `_decrement!` applied the amount twice
|
|
825
|
+
to the local value, and `op_increment!` truncated fractional amounts.
|
|
826
|
+
Changes to array fields were never marked saved after a save, so the next
|
|
827
|
+
save resent the whole array.
|
|
828
|
+
- **FIXED**: `Parse::Object.new` converts only strong parameters, Structs,
|
|
829
|
+
and OpenStructs through `to_h` (permitted `ActionController::Parameters`
|
|
830
|
+
now work; unpermitted ones still raise), and with
|
|
831
|
+
`strict_property_redefinition = false` the redeclaration warning reports
|
|
832
|
+
the existing type rather than the new one.
|
|
833
|
+
- **FIXED**: The cache middleware's rescue named Redis error classes
|
|
834
|
+
directly, so with a non-Redis store any other store error became
|
|
835
|
+
`NameError: uninitialized constant Redis`. Redis and `connection_pool`
|
|
836
|
+
classes are listed only when loaded, and `Redis::BaseConnectionError`,
|
|
837
|
+
`Redis::ReadOnlyError`, `RedisClient::Error`, and `Errno::ECONNREFUSED` now
|
|
838
|
+
disable caching for the request instead of failing it.
|
|
839
|
+
|
|
840
|
+
#### ACL edits and revocations are always saved
|
|
841
|
+
|
|
842
|
+
- **FIXED**: Revoking an ACL grant after a save was not sent: a stale
|
|
843
|
+
pre-save snapshot made the revocation compare unchanged, and `save`
|
|
844
|
+
returned true without writing it.
|
|
845
|
+
- **FIXED**: `rollback!` now undoes in-place ACL edits. Copying an ACL shared
|
|
846
|
+
its permissions table, so a rolled-back grant was still sent on the next
|
|
847
|
+
save and change history showed identical old and new ACLs.
|
|
848
|
+
- **FIXED**: `acl.delete(:public)`, `acl.delete("*")`, and `acl.delete(role)`
|
|
849
|
+
remove the entry (only user ids matched before), and
|
|
850
|
+
`Permission#read!`/`#write!`/`#no_read!`/`#no_write!` mark the ACL changed,
|
|
851
|
+
so a revoke through a `Permission` on a fetched object is saved.
|
|
852
|
+
- **FIXED**: Batch (`Array#save`) and transaction saves apply the
|
|
853
|
+
`acl_policy` owner ACL to new records; owner-only records were created
|
|
854
|
+
public.
|
|
855
|
+
- **FIXED**: `apply_role("role:Admin")` no longer writes `role:role:Admin`,
|
|
856
|
+
the string `"false"` is a denial rather than a grant, and ACL `eql?`/`hash`
|
|
857
|
+
agree with `==`.
|
|
858
|
+
|
|
859
|
+
#### Batches, transactions, and retries apply exactly what succeeded
|
|
860
|
+
|
|
861
|
+
- **FIXED**: Transactions are sent as one `POST /batch` with
|
|
862
|
+
`transaction: true`. The flag was dropped and transactions over 50
|
|
863
|
+
operations were split into parallel batches, so a failure could leave a
|
|
864
|
+
partial commit while the SDK rolled local state back. The retry on
|
|
865
|
+
conflict code 251 now runs.
|
|
866
|
+
- **FIXED**: A batch chunk that fails with an HTTP error or a malformed
|
|
867
|
+
response fails every request in that chunk; before, later responses
|
|
868
|
+
shifted onto the wrong objects, and a short or non-array response counted
|
|
869
|
+
as success with objects marked clean but missing ids. Identical requests
|
|
870
|
+
(two Increments) are no longer collapsed into one, an object whose
|
|
871
|
+
attribute write and relation write split keeps the failed part dirty, a
|
|
872
|
+
batch create sets `updated_at`, `Array#destroy` marks objects destroyed,
|
|
873
|
+
new objects' relation additions are sent in the create, and a success body
|
|
874
|
+
with a `code` or `error` column is no longer treated as a failure.
|
|
875
|
+
- **FIXED**: `save_all` reports a failing last page, visits records the block
|
|
876
|
+
leaves unchanged instead of stopping early, no longer mutates the caller's
|
|
877
|
+
constraints, and a query-scoped `save_all` with a block no longer runs a
|
|
878
|
+
second forced save over every match.
|
|
879
|
+
- **FIXED**: A 502 or 504 with a JSON body that has no Parse error (as from a
|
|
880
|
+
gateway or load balancer) raises `Parse::Error::ServiceUnavailableError`;
|
|
881
|
+
before, it counted as success and saves returned true with no id.
|
|
882
|
+
- **FIXED**: With `assume_server_idempotency`, retries only replay routes
|
|
883
|
+
Parse Server deduplicates (object, user, and installation writes,
|
|
884
|
+
functions, jobs); `POST /batch`, files, push, login, schemas, and config
|
|
885
|
+
are never replayed. Automatic request ids are left off functions, jobs,
|
|
886
|
+
push, logout, sessions, events, and password reset (the exclusion never
|
|
887
|
+
matched). A retried DELETE that finds the object gone reports success,
|
|
888
|
+
bodies with `__op` at any depth are not retried, error 143 is no longer a
|
|
889
|
+
`TimeoutError`, `retry: 0` disables retries, and the caller's headers hash
|
|
890
|
+
is no longer modified.
|
|
891
|
+
- **CHANGED**: The `first_or_create!(synchronize: true)` lock lease defaults
|
|
892
|
+
to the client's worst-case time for a find and a create with retries
|
|
893
|
+
(about 222s with default timeouts) instead of 3 seconds, so it no longer
|
|
894
|
+
expires mid-call and admits a duplicate. The maximum `ttl:` rises from 30 to
|
|
895
|
+
300 seconds.
|
|
896
|
+
|
|
897
|
+
#### Queries return the rows they describe
|
|
898
|
+
|
|
899
|
+
- **FIXED**: `or_where` and `|` keep constraints added next to an existing
|
|
900
|
+
`$or`, and `Parse::Query.and` keeps every `$or` it combines.
|
|
901
|
+
- **FIXED**: Automatic paging (`limit(:max)`, `all`, `results { }`) appends
|
|
902
|
+
`objectId` as the final sort key, so rows that tie on the sort field are no
|
|
903
|
+
longer returned twice or skipped (live: 1,200 rows returned, 617 unique).
|
|
904
|
+
- **FIXED**: `latest` and `last_updated` work on a query that already has an
|
|
905
|
+
order, and `first`, `latest`, and `last_updated` no longer change the query
|
|
906
|
+
they are called on.
|
|
907
|
+
- **FIXED**: A bare objectId compared to a pointer field in aggregate and
|
|
908
|
+
mongo-direct queries is matched in pointer storage form (`$ne` matched every
|
|
909
|
+
row; equality matched none), and a `Set` or `Range` passed to `in`, `nin`,
|
|
910
|
+
`all`, or `contained_by` is expanded into its members.
|
|
911
|
+
- **FIXED**: `Parse::Model.find_class` no longer returns a class left behind
|
|
912
|
+
by `remove_const` after a model is redefined (code reload, tests), so field
|
|
913
|
+
aliases and `_p_` pointer detection use the current class, and a class named
|
|
914
|
+
after an earlier lookup is found. `belongs_to` and `has_many` refresh the
|
|
915
|
+
cached aliases, and linked-pointer constraints use the linked class's
|
|
916
|
+
declared names.
|
|
917
|
+
|
|
918
|
+
#### The gem coexists with Rails and other libraries
|
|
919
|
+
|
|
920
|
+
- **FIXED**: The gem loads when `Rails` is defined without railties (for
|
|
921
|
+
example by rails-html-sanitizer).
|
|
922
|
+
- **CHANGED**: Query DSL methods on `Symbol` (`:plays.gt`, `:name.desc`) live
|
|
923
|
+
in included modules, so a method Ruby or another library (Mongoid, Sequel
|
|
924
|
+
core extensions) defines on `Symbol` is never replaced, whichever loads
|
|
925
|
+
first. Skipped names are listed in `Parse::Operation.symbol_conflicts`, and
|
|
926
|
+
Mongoid query keys passed to a Parse query are translated.
|
|
927
|
+
- **FIXED**: Automatic pluralized aliases are created only in the namespace
|
|
928
|
+
that defines the model (`::Posts` for `Post`), never in the module where
|
|
929
|
+
the lookup happened, and a frozen namespace no longer raises `FrozenError`.
|
|
930
|
+
- **FIXED**: Schema migration no longer crashes on relation fields, creates
|
|
931
|
+
Pointer and Relation columns with their `targetClass`, creates `:vector`
|
|
932
|
+
columns as Array, and no longer reports `:vector`, `:timezone`, `:phone`,
|
|
933
|
+
or `:email` fields as mismatched.
|
|
934
|
+
- **FIXED**: `Parse.auto_generate_models!` exposes a server column that
|
|
935
|
+
clashes with a Ruby method (`class`, `hash`, `send`) as `<name>_field`,
|
|
936
|
+
skips a second column that underscores to an existing name with a warning
|
|
937
|
+
instead of aborting, and generated classes query their real server class.
|
|
938
|
+
- **FIXED**: Search index migrations, `describe(:atlas)`, and the agent's
|
|
939
|
+
Atlas tools load Atlas Search on first use instead of failing with
|
|
940
|
+
`NameError`. `enable_mcp!` no longer needs webrick (not a dependency), and
|
|
941
|
+
prompt-marker scrubbing works without the MCP client loaded.
|
|
942
|
+
- **CHANGED**: `Parse::User.model_name` is relative to the `Parse`
|
|
943
|
+
namespace, so Rails forms and routes use `params[:user]` and `users_path`.
|
|
944
|
+
|
|
945
|
+
#### Sessions, MFA, and the response cache
|
|
946
|
+
|
|
947
|
+
- **FIXED**: `login`, `login_with_mfa`, `verify_password`,
|
|
948
|
+
`request_password_reset`, and `request_email_verification` are always
|
|
949
|
+
sent without the master key or any session token. With the master key,
|
|
950
|
+
Parse Server skipped MFA validation and saved the submitted
|
|
951
|
+
`authData.mfa` over the enrolled TOTP secret, so any code was accepted and
|
|
952
|
+
MFA was silently disabled for the account.
|
|
953
|
+
- **FIXED**: An explicit session token (a call's `session_token:` or an
|
|
954
|
+
`X-Parse-Session-Token` header) is never replaced by the ambient
|
|
955
|
+
`with_session` token or a client-bound token. `Parse::User.session(a)`
|
|
956
|
+
inside `with_session(b)` returned user B and cached token A as B.
|
|
957
|
+
- **FIXED**: Logout, `logout_all!`, session destroy, password change, user
|
|
958
|
+
deletion, and `Role` saves invalidate the cached identity and role
|
|
959
|
+
closures, so revocations and role changes reach mongo-direct and Atlas
|
|
960
|
+
paths immediately instead of after the identity or role TTL; atomic
|
|
961
|
+
`role.users` and `role.roles` operations invalidate the same caches. The MCP
|
|
962
|
+
session check treats Parse Server's answer for a revoked token (HTTP 400,
|
|
963
|
+
code 209) as a rejection.
|
|
964
|
+
- **FIXED**: The response cache never stores `users/me`, `sessions/me`,
|
|
965
|
+
`login`, `verifyPassword`, or `logout`. Cache keys in both layouts carry
|
|
966
|
+
the application id and a digest of the credential sent, so a client with
|
|
967
|
+
other keys sharing the store cannot read another's entries. A write
|
|
968
|
+
retires every cached variant of the resource and every cached query over
|
|
969
|
+
its class (including batch sub-requests), so a revoked row is not served
|
|
970
|
+
from a cached query until it expires. Reads establish their cache versions
|
|
971
|
+
before the request and skip storing a response when a version changed in
|
|
972
|
+
flight, so a read that overlaps an ACL write cannot restore the revoked
|
|
973
|
+
row.
|
|
974
|
+
- **FIXED**: `Parse::Client#send_request` honors the request's own
|
|
975
|
+
`session_token:`, `use_master_key:`, `cache:`, and `retry:` options; a
|
|
976
|
+
request opting out of the master key still sent it.
|
|
977
|
+
- **FIXED**: A password-only login on an MFA account raises
|
|
978
|
+
`Parse::MFA::RequiredError`, and a wrong code raises
|
|
979
|
+
`Parse::MFA::VerificationError`, instead of `ServiceUnavailableError`.
|
|
980
|
+
- **CHANGED**: Session tokens are redacted from `Parse::User`,
|
|
981
|
+
`Parse::Session`, and `Parse::Query` `inspect` output, and
|
|
982
|
+
`Parse::Session#as_json` omits `sessionToken` unless
|
|
983
|
+
`include_session_token: true` is passed.
|
|
984
|
+
|
|
985
|
+
#### Mongo-direct reads match REST access rules
|
|
986
|
+
|
|
987
|
+
- **FIXED**: `readUserFields` and pointer-permission CLPs are enforced on
|
|
988
|
+
mongo-direct, Atlas Search, vector, and hybrid reads (every authenticated
|
|
989
|
+
user saw every row) and are applied before `$skip`/`$limit`/`$count`, so
|
|
990
|
+
`count_direct` and pages are correct. Joins into another class apply that
|
|
991
|
+
class's ownership rules and fail closed on a denied CLP.
|
|
992
|
+
- **FIXED**: A user keeps their own protected `_User` fields only when no
|
|
993
|
+
pipeline stage can rewrite `_id`; a pipeline that set another user's `_id`
|
|
994
|
+
to the caller's exposed that user's email.
|
|
995
|
+
- **FIXED**: Direct queries inside `with_session` were denied by CLP because
|
|
996
|
+
the schema lookup sent the session token; it now uses the master key.
|
|
997
|
+
- **FIXED**: `$inQuery`, `$notInQuery`, `$select`, `$dontSelect`, and
|
|
998
|
+
`$containedBy` work on `results_direct` and `count_direct`, and direct rows
|
|
999
|
+
decode like REST: included objects carry their class, `includes` with
|
|
1000
|
+
`keys` keeps them, File columns decode as files, and a dotted key returns
|
|
1001
|
+
its whole column.
|
|
1002
|
+
- **CHANGED**: protectedFields stripping removes top-level columns only,
|
|
1003
|
+
matching Parse Server.
|
|
1004
|
+
- **FIXED**: Scoped mongo-direct pipelines on classes with protected fields
|
|
1005
|
+
accept a `$getField`, `$setField`, or `$unsetField` name only as a plain
|
|
1006
|
+
string or a `$literal` string, so a name read from the document cannot
|
|
1007
|
+
alias a protected field.
|
|
1008
|
+
- **FIXED**: `results_direct`, `count_direct`, `distinct_direct`, the
|
|
1009
|
+
mongo-direct auto-route, and the Atlas Search bridge respect
|
|
1010
|
+
`Parse.without_master_key`, resolving to the session or public scope as
|
|
1011
|
+
REST does instead of reading as master.
|
|
1012
|
+
- **FIXED**: Subqueries (`in_query`, `not_in_query`, `select`,
|
|
1013
|
+
`dont_select`) combined with `or_where` or nested in `$and`, `$or`, or
|
|
1014
|
+
`$nor` compile to their own joins on the direct path, with the joined
|
|
1015
|
+
class's ACL, CLP, and protected-field checks. A subquery in a position that
|
|
1016
|
+
cannot be translated, or nested in a query-derived aggregation, raises
|
|
1017
|
+
`ArgumentError` instead of reaching MongoDB raw (one under `$not` used to
|
|
1018
|
+
invert the filter).
|
|
1019
|
+
|
|
1020
|
+
#### Webhooks follow Parse Server's contract
|
|
1021
|
+
|
|
1022
|
+
- **FIXED**: `before_delete` can deny a delete, and `after_destroy` fires for
|
|
1023
|
+
afterDelete webhooks. `Payload#parse_query` reads constraints only from
|
|
1024
|
+
`where` and applies `limit`, `skip`, `order`, `keys`, and `include`. The
|
|
1025
|
+
response log is redacted, an unexpected handler exception returns a JSON
|
|
1026
|
+
error without its message, and a trigger whose body names a different
|
|
1027
|
+
class than its URL is refused.
|
|
1028
|
+
- **FIXED**: A signed webhook delivery is deduplicated on its signature, so a
|
|
1029
|
+
captured request cannot be replayed within the timestamp window by
|
|
1030
|
+
altering or dropping the unsigned nonce. A signature may cover the
|
|
1031
|
+
delivery nonce (`"#{timestamp}.#{nonce}.#{body}"`), so identical bodies
|
|
1032
|
+
sent in the same second each get their own signature.
|
|
1033
|
+
- **NEW**: `error!(message, code:)` and `Parse::Webhooks::ResponseError#code`
|
|
1034
|
+
carry a Parse error code (Parse Server's HTTP adapter still reports 141).
|
|
1035
|
+
|
|
1036
|
+
#### Associations save exactly what changed
|
|
1037
|
+
|
|
1038
|
+
- **FIXED**: Relation additions and removals are cleared after a save and
|
|
1039
|
+
deduplicated (a stale `RemoveRelation` was resent on every later save),
|
|
1040
|
+
staged without loading the relation, and never queried with a null owner.
|
|
1041
|
+
A relation declared with `field:` reads the remote column, and `rollback!`
|
|
1042
|
+
works on relations.
|
|
1043
|
+
- **FIXED**: `clear` on an array collection is saved, and atomic
|
|
1044
|
+
`add!`/`add_unique!`/`remove!` keep the local array in step with the
|
|
1045
|
+
server instead of emptying it (the next save overwrote the server array).
|
|
1046
|
+
A clean array adopts the array the server returns; unsaved local edits are
|
|
1047
|
+
kept and still sent.
|
|
1048
|
+
- **FIXED**: Nested partial fetches cover multi-word `belongs_to` fields, a
|
|
1049
|
+
query `has_many` on an unsaved owner returns a chainable empty query, and
|
|
1050
|
+
`CollectionProxy#replace` is added.
|
|
1051
|
+
- **FIXED**: Reading `acl` on an object fetched with `keys:` that left the
|
|
1052
|
+
ACL out fetches the stored ACL instead of returning nil, so code that
|
|
1053
|
+
edits it cannot replace the record's real ACL. Saving other fields never
|
|
1054
|
+
sends the ACL, and the partial-fetch tracking survives an update, so the
|
|
1055
|
+
ACL is still fetched on read after a save. Reassigning a property to itself
|
|
1056
|
+
after an in-place edit keeps the edit.
|
|
1057
|
+
|
|
1058
|
+
#### `protectedFields` resolution matches Parse Server
|
|
1059
|
+
|
|
1060
|
+
- **FIXED**: `Parse::CLPScope.protected_fields_for` now resolves
|
|
1061
|
+
`protectedFields` the way Parse Server does: it intersects only the groups
|
|
1062
|
+
present in the map that apply to the caller (`*`, `authenticated`, each
|
|
1063
|
+
`role:` claim, and the caller's user id). Previously a map with no `"*"`
|
|
1064
|
+
entry started from an empty set and intersected every role and user entry
|
|
1065
|
+
away, so `{ "role:Restricted" => ["secret"] }` stripped nothing for a member
|
|
1066
|
+
of `Restricted`. Mongo-direct queries, Atlas Search, vector search, and
|
|
1067
|
+
pipeline validation could therefore return fields the server would have
|
|
1068
|
+
hidden. The `authenticated` group is now applied too.
|
|
1069
|
+
- **CHANGED**: The integration stack pins Parse Server 9.10.3 (was 9.10.0)
|
|
1070
|
+
and enables `enableLiveQueryClassLevelPermissionRoles`. New integration
|
|
1071
|
+
coverage checks empty versus omitted `username` and `password` on `_User`
|
|
1072
|
+
updates, refusal of anonymous, cross-user, and redirected `_User` updates,
|
|
1073
|
+
`_Session` create honoring the class's `create` and `addField`
|
|
1074
|
+
permissions, and role-granted LiveQuery subscriptions.
|
|
1075
|
+
|
|
1076
|
+
#### Release checks prove the intended coverage ran
|
|
1077
|
+
|
|
1078
|
+
- **CHANGED**: `rake test:integration` checks Parse Server's health before
|
|
1079
|
+
each file and after each failure, and fails the run (naming the file) when
|
|
1080
|
+
the server goes down instead of passing with skips. Skips caused by an
|
|
1081
|
+
unreachable Parse Server, MongoDB, or Redis fail their file; Atlas-only,
|
|
1082
|
+
missing-credential, and missing-tool skips remain skips.
|
|
1083
|
+
`PSNEXT_FAIL_ON_INFRA_SKIP=false` reports them without failing.
|
|
1084
|
+
- **FIXED**: The integration stack's Parse Server cache adapter could crash
|
|
1085
|
+
Parse Server when its Redis client was closed (an unawaited rejected `put`
|
|
1086
|
+
became an uncaught exception). Cache commands now connect lazily, wait for
|
|
1087
|
+
readiness with a bound, and treat any command error as a cache miss.
|
|
1088
|
+
- **NEW**: The release workflow validates the tagged commit before
|
|
1089
|
+
publishing: the tag must match `version.rb`, the unit-test workflow must
|
|
1090
|
+
have passed on that exact commit, and the unit suite runs again; otherwise
|
|
1091
|
+
nothing is pushed. Provider contract tests run in a separate scheduled or
|
|
1092
|
+
manual workflow, never on pull requests.
|
|
1093
|
+
- **NEW**: CI runs the unit suite against ActiveModel/ActiveSupport 7.1, 7.2,
|
|
1094
|
+
and 8.0 (`gemfiles/`), and an MCP client smoke test drives the Rack app
|
|
1095
|
+
over real HTTP through initialize, tool and prompt listing, completion,
|
|
1096
|
+
logging, a streamed tool call, and session termination.
|
|
1097
|
+
- **NEW**: The MongoDB direct guide documents MongoDB 9.0 `null` semantics on
|
|
1098
|
+
dotted paths. The SDK never compares a dotted path to `null`
|
|
1099
|
+
on its own; only caller-supplied constraints are affected, and those
|
|
1100
|
+
shapes are pinned by tests.
|
|
1101
|
+
|
|
1102
|
+
#### Query model resolution works for any Parse class name
|
|
1103
|
+
|
|
1104
|
+
- **FIXED**: `Parse::Query` looked up its table's model with
|
|
1105
|
+
`Parse::Model.const_get`, which fails for Parse class names that are not
|
|
1106
|
+
Ruby constants (`"contacts"`, `"_User"`) and misses models whose
|
|
1107
|
+
`parse_class` differs from the Ruby class name. The error was rescued to
|
|
1108
|
+
nil, so pointer fields were silently treated as plain fields: mongo-direct
|
|
1109
|
+
pipelines addressed `owner` instead of `_p_owner` and pointer values were
|
|
1110
|
+
not converted to storage form. Models now resolve through
|
|
1111
|
+
`Parse::Model.find_class`.
|
|
1112
|
+
|
|
1113
|
+
### Behavior Notes
|
|
1114
|
+
|
|
1115
|
+
- An Atlas vector index whose path is the Ruby name of a multi-word
|
|
1116
|
+
`:vector` property (`body_embedding`) must be recreated with the stored
|
|
1117
|
+
column (`bodyEmbedding`); drift detection reports the mismatch. Such an
|
|
1118
|
+
index never matched stored vectors, so search results do not get worse.
|
|
1119
|
+
- On a shared master-key endpoint without a `principal_resolver`, every
|
|
1120
|
+
caller is the same principal, so owner binding does not separate them; use
|
|
1121
|
+
`MCPRackApp.master_analytics`, which requires a resolver.
|
|
1122
|
+
- `Parse::Authorization` resolves a session through `/users/me` with the
|
|
1123
|
+
response cache bypassed, adding one uncached round trip per identity-cache
|
|
1124
|
+
miss.
|
|
1125
|
+
- Field policies, field-name mode, and query field aliases live in fiber
|
|
1126
|
+
storage for each tool call or query, so a thread or fiber a custom tool starts inherits them. A thread
|
|
1127
|
+
created before the call (a long-lived pool) does not, and runs unnarrowed.
|
|
1128
|
+
- Without a tenant scope, every `user_scoped` caller charges the shared
|
|
1129
|
+
default `SpendCap` bucket, so one user can use up the embedding and
|
|
1130
|
+
reranking budget for all. Declare `agent_tenant_scope` on the searched
|
|
1131
|
+
classes and pass `tenant_from:` so each tenant has its own budget.
|
|
1132
|
+
- `Parse::Object.transaction` now really runs as a Parse Server transaction,
|
|
1133
|
+
which needs MongoDB to be a replica set or mongos. On a standalone server it
|
|
1134
|
+
fails with a 500 instead of running without atomicity, and the raised
|
|
1135
|
+
`Parse::Error` says so and points to `Array#save` for a non-atomic batch.
|
|
1136
|
+
Parse Server runs a transaction's requests concurrently on one session,
|
|
1137
|
+
which MongoDB intermittently rejects with a bare 500. Because a 500 does
|
|
1138
|
+
not prove the transaction was not applied (a failed commit looks the
|
|
1139
|
+
same), the SDK resends only on a 251 conflict by default;
|
|
1140
|
+
`transaction(retry_server_errors: true)` also resends on a 500 for writes
|
|
1141
|
+
that are safe to repeat. A 502, 503, or 504 is never resent.
|
|
1142
|
+
- Mongo-direct, Atlas Search, and vector results for callers in a `role:` or
|
|
1143
|
+
`authenticated` protectedFields group now omit those fields, matching
|
|
1144
|
+
Parse Server.
|
|
1145
|
+
- Parse Server 9.10.3 honors `role:` entries in class-level permissions for
|
|
1146
|
+
LiveQuery subscriptions only when `enableLiveQueryClassLevelPermissionRoles`
|
|
1147
|
+
is set (default `false`). Without it, a role member is refused a
|
|
1148
|
+
subscription the equivalent REST query would serve, which affects MCP
|
|
1149
|
+
`resources/subscribe` on such classes.
|
|
1150
|
+
|
|
3
1151
|
### 5.7.6
|
|
4
1152
|
|
|
5
1153
|
#### `semantic_search` no longer returns hidden fields as chunk content
|