parse-stack-next 5.1.1 → 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.env.sample +12 -0
- data/.env.test +4 -4
- data/CHANGELOG.md +545 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +6 -1
- data/README.md +167 -38
- data/Rakefile +56 -10
- data/docs/atlas_vector_search_guide.md +110 -9
- data/docs/mcp_guide.md +433 -0
- data/docs/mongodb_direct_guide.md +66 -1
- data/docs/mongodb_index_optimization_guide.md +22 -1
- data/docs/usage_guide.md +15 -0
- data/lib/parse/agent/approval_gate.rb +0 -0
- data/lib/parse/agent/constraint_translator.rb +90 -19
- data/lib/parse/agent/describe.rb +1 -0
- data/lib/parse/agent/errors.rb +16 -0
- data/lib/parse/agent/mcp_client.rb +9 -0
- data/lib/parse/agent/mcp_dispatcher.rb +139 -7
- data/lib/parse/agent/mcp_rack_app.rb +621 -17
- data/lib/parse/agent/mcp_subscriptions.rb +607 -0
- data/lib/parse/agent/metadata_dsl.rb +58 -0
- data/lib/parse/agent/metadata_registry.rb +141 -1
- data/lib/parse/agent/prompt_hardening.rb +213 -0
- data/lib/parse/agent/result_formatter.rb +18 -3
- data/lib/parse/agent/tools.rb +167 -24
- data/lib/parse/agent.rb +692 -21
- data/lib/parse/client/request.rb +55 -4
- data/lib/parse/client/response.rb +4 -0
- data/lib/parse/client.rb +205 -7
- data/lib/parse/model/classes/installation.rb +27 -10
- data/lib/parse/model/classes/user.rb +8 -0
- data/lib/parse/model/core/actions.rb +58 -4
- data/lib/parse/model/core/embed_managed.rb +19 -14
- data/lib/parse/model/core/indexing.rb +108 -16
- data/lib/parse/model/core/querying.rb +29 -0
- data/lib/parse/model/model.rb +34 -3
- data/lib/parse/model/object.rb +1 -0
- data/lib/parse/query.rb +90 -24
- data/lib/parse/retrieval/agent_tool.rb +369 -0
- data/lib/parse/retrieval/chunk.rb +74 -0
- data/lib/parse/retrieval/chunker.rb +208 -0
- data/lib/parse/retrieval/retriever.rb +274 -0
- data/lib/parse/retrieval.rb +10 -0
- data/lib/parse/schema.rb +69 -20
- data/lib/parse/stack/version.rb +2 -2
- data/parse-stack-next.gemspec +1 -1
- data/scripts/docker/docker-compose.atlas.yml +14 -10
- data/scripts/docker/docker-compose.test.yml +24 -20
- data/scripts/docker/mongo-init.js +3 -3
- data/scripts/start-parse.sh +10 -0
- data/scripts/start_mcp_server.rb +1 -1
- data/scripts/test_server_connection.rb +1 -1
- data/scripts/vector_prototype/create_vector_index.js +1 -1
- data/scripts/vector_prototype/fetch_embeddings.py +2 -2
- data/scripts/vector_prototype/query_prototype.rb +1 -1
- data/scripts/vector_prototype/run.sh +4 -4
- metadata +10 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 25238eea832d32b8c536522ec1c43d087b3db8e996b13d39430c2edfd74481ee
|
|
4
|
+
data.tar.gz: 806f28241cfbfe7e77fa230bd2e6b2b4aa071b89b7dda828f52d0b5182a7122c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b85adcba96ac6023989bf42e8676eee9bb22b4ef0cb02073cc7ed0b79078c3edd88a84ff6f0b6c6f15eda214ba6a9c3165d35c7c250ef1ecfcd0471f58c3929c
|
|
7
|
+
data.tar.gz: 9646a8b0f4bcd679a373d8a3ba189f0f160a7227280690b2ca165987b535f0c0ac6972dcf04d053ba441b732f80462698d7e1e08365c4419368aeea4481117c6
|
data/.env.sample
CHANGED
|
@@ -38,6 +38,18 @@ PARSE_MASTER_KEY=myMasterKey
|
|
|
38
38
|
# PARSE_LIVE_QUERY_URL=
|
|
39
39
|
# HOOKS_URL=
|
|
40
40
|
|
|
41
|
+
# ===========================================================================
|
|
42
|
+
# Test harness — uses an ISOLATED stack (not the values above)
|
|
43
|
+
#
|
|
44
|
+
# The integration test suite does NOT connect with the defaults above. It runs
|
|
45
|
+
# on a deliberately isolated Docker stack — a private 29xxx host-port block, a
|
|
46
|
+
# dedicated Compose project `psnext-it`, and the `parse_stack_next_it` database
|
|
47
|
+
# — so it never collides with another Parse system on the same host. The full
|
|
48
|
+
# value set is listed in `.env.test` (a committed reference; nothing auto-loads
|
|
49
|
+
# it — source it or export the vars). The full port map and override variables
|
|
50
|
+
# are documented in the "Isolated test environment" section of CLAUDE.md.
|
|
51
|
+
# ===========================================================================
|
|
52
|
+
|
|
41
53
|
# ===========================================================================
|
|
42
54
|
# Webhooks
|
|
43
55
|
# ===========================================================================
|
data/.env.test
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Parse Server Test Configuration
|
|
2
|
-
PARSE_TEST_SERVER_URL=http://localhost:
|
|
3
|
-
PARSE_TEST_APP_ID=
|
|
4
|
-
PARSE_TEST_API_KEY=
|
|
5
|
-
PARSE_TEST_MASTER_KEY=
|
|
2
|
+
PARSE_TEST_SERVER_URL=http://localhost:29337/parse
|
|
3
|
+
PARSE_TEST_APP_ID=psnextItAppId
|
|
4
|
+
PARSE_TEST_API_KEY=psnext-it-rest-key
|
|
5
|
+
PARSE_TEST_MASTER_KEY=psnextItMasterKey
|
|
6
6
|
|
|
7
7
|
# Docker Configuration
|
|
8
8
|
PARSE_TEST_USE_DOCKER=true
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,550 @@
|
|
|
1
1
|
## parse-stack-next Changelog
|
|
2
2
|
|
|
3
|
+
### 5.2.0
|
|
4
|
+
|
|
5
|
+
#### Retrieval layer — `Parse::Retrieval` (`Parse::RAG`)
|
|
6
|
+
|
|
7
|
+
A safe-by-default retrieval-augmented-generation path on top of the existing
|
|
8
|
+
vector-search stack. `Parse::Retrieval.retrieve` embeds a natural-language
|
|
9
|
+
query, runs Atlas `$vectorSearch` through the existing ACL/CLP-enforcing
|
|
10
|
+
`find_similar`, and splits each retrieved document's text field into scored,
|
|
11
|
+
citable chunks. Chunking is a presentation step applied after retrieval —
|
|
12
|
+
embedding remains one-vector-per-record, so every chunk inherits its parent
|
|
13
|
+
document's single score.
|
|
14
|
+
|
|
15
|
+
- **NEW**: `Parse::Retrieval::Chunker::FixedSizeOverlap(size:, overlap:, by:, max_chunks_per_document:)`
|
|
16
|
+
— a fixed-size sliding-window chunker with overlap, `by: :chars` (default) or
|
|
17
|
+
`by: :tokens`. Subclass `Parse::Retrieval::Chunker::Base` for custom
|
|
18
|
+
strategies. The `max_chunks_per_document` cap (default 200) truncates with a
|
|
19
|
+
signal rather than raising, bounding the one-document-to-many-chunks
|
|
20
|
+
amplification surface. (`lib/parse/retrieval/chunker.rb`)
|
|
21
|
+
- **NEW**: `Parse::Retrieval.retrieve(query:, klass:, field:, k:, filter:, vector_filter:, chunker:, tenant_scope:, score_quantize:, **scope_opts)`
|
|
22
|
+
returns `Array<Parse::Retrieval::Chunk>` (`{ id, score, content, source, metadata }`).
|
|
23
|
+
ACL is enforced mongo-direct inside `find_similar`; scope kwargs
|
|
24
|
+
(`session_token:` / `acl_user:` / `acl_role:` / `master:`) pass through. A
|
|
25
|
+
tenant scope is merged into the Atlas pre-filter, closing the cross-tenant
|
|
26
|
+
existence side channel. (`lib/parse/retrieval/retriever.rb`)
|
|
27
|
+
- **NEW**: `agent_searchable field:, filter_fields:` model macro opts a class in
|
|
28
|
+
to the agent retrieval tool and declares which fields an agent may filter on.
|
|
29
|
+
(`lib/parse/agent/metadata_dsl.rb`)
|
|
30
|
+
- **NEW**: `semantic_search` agent tool (`permission: :readonly`,
|
|
31
|
+
`client_safe: true`) routing through `Parse::Retrieval.retrieve` with the full
|
|
32
|
+
agent security envelope: searchable-class allowlist, recursive underscore-key
|
|
33
|
+
refusal and filter-field allowlist on caller input, `field_allowlist`
|
|
34
|
+
projection plus tenant-scope re-assertion on every returned record, and score
|
|
35
|
+
quantization in non-admin contexts. (`lib/parse/retrieval/agent_tool.rb`)
|
|
36
|
+
- **NEW**: `semantic_search` accepts `text_field:` to pick which embedded text
|
|
37
|
+
source to chunk and return as `content` — required for models that embed more
|
|
38
|
+
than one text field (previously such a model raised `AmbiguousTextField` on
|
|
39
|
+
every call with no way to disambiguate from the tool). The value is
|
|
40
|
+
constrained to the class's declared `embed` sources so it can't surface a
|
|
41
|
+
field the model never opted into embedding.
|
|
42
|
+
- **FIXED**: `semantic_search` now forwards `max_chunks_per_document` to the
|
|
43
|
+
chunker (it was silently dropped on the agent path, so the per-document chunk
|
|
44
|
+
cap could not be tuned through the tool). (`lib/parse/retrieval/agent_tool.rb`)
|
|
45
|
+
- **IMPROVED**: parameter-name aliases smooth over inconsistencies across the
|
|
46
|
+
surface — `Parse::Agent.new` accepts `permission:` (alias of `permissions:`)
|
|
47
|
+
and `impersonation_user:` / `impersonation_mint:` / `impersonate_label:`
|
|
48
|
+
(aliases of `impersonate_user:` / `impersonate_mint:` / `impersonation_label:`);
|
|
49
|
+
`Parse::Agent::Tools.register` accepts `permissions:` (alias of `permission:`);
|
|
50
|
+
and `semantic_search` accepts `klass:` / `class:` for `class_name` and the
|
|
51
|
+
chunker's own `size:` / `overlap:` / `by:` names. Canonical names are
|
|
52
|
+
unchanged. (`lib/parse/agent.rb`, `lib/parse/agent/tools.rb`,
|
|
53
|
+
`lib/parse/retrieval/agent_tool.rb`)
|
|
54
|
+
- **NEW**: `Parse::RAG` is a discoverability alias for `Parse::Retrieval`.
|
|
55
|
+
- **NEW**: `rerank:` and `hybrid:` are reserved on `retrieve` and raise
|
|
56
|
+
`NotImplementedError` if supplied, locking the API shape for later releases.
|
|
57
|
+
|
|
58
|
+
#### MCP elicitation — human-in-the-loop approval for destructive operations
|
|
59
|
+
|
|
60
|
+
`:write` / `:admin` tier tool calls can now require human approval before they
|
|
61
|
+
run, using the MCP 2025-06-18 spec-native `elicitation/create` channel. The
|
|
62
|
+
server sends the proposed dry-run diff to the client over the listening stream
|
|
63
|
+
and blocks until the approver accepts or rejects.
|
|
64
|
+
|
|
65
|
+
- **NEW**: `Parse::Agent.require_approval_for = [:write, :admin]` opts tiers into
|
|
66
|
+
approval. Off by default, so existing clients are unaffected.
|
|
67
|
+
- **NEW**: A pluggable approval gate (`Parse::Agent#approval_gate`) consulted by
|
|
68
|
+
`Parse::Agent#execute`, reachable on the non-MCP path and unit-testable with a
|
|
69
|
+
fake approver. `Parse::Agent::MCPElicitationGate` is the spec-native
|
|
70
|
+
implementation; `Parse::Agent::NullGate` (the default) approves.
|
|
71
|
+
(`lib/parse/agent/approval_gate.rb`)
|
|
72
|
+
- **NEW**: `call_method` resolves the *effective* tier from the target
|
|
73
|
+
`agent_method`'s declared permission, so write/admin methods invoked through
|
|
74
|
+
the readonly `call_method` tool are gated correctly. The approval diff reuses
|
|
75
|
+
the existing dry-run preview.
|
|
76
|
+
- **NEW**: MCPRackApp captures the client's `elicitation` capability at
|
|
77
|
+
`initialize`, routes the client's reply (a method-less JSON-RPC response) into
|
|
78
|
+
a session-bound pending registry, and accepts an `approval_timeout:`.
|
|
79
|
+
(`lib/parse/agent/mcp_rack_app.rb`, `lib/parse/agent/mcp_dispatcher.rb`,
|
|
80
|
+
`lib/parse/agent/mcp_subscriptions.rb`)
|
|
81
|
+
- **SECURITY**: fails closed — when approval is required but the client did not
|
|
82
|
+
advertise the capability, no listening stream is open, the transport is
|
|
83
|
+
non-streaming, or the approver times out, the destructive operation is
|
|
84
|
+
refused, never silently executed. Replies are session-bound so one session
|
|
85
|
+
cannot answer another's prompt.
|
|
86
|
+
|
|
87
|
+
#### Agent roadmap: impersonation, prompt hardening, telemetry, provenance
|
|
88
|
+
|
|
89
|
+
- **NEW**: Agent impersonation for user-scoped queries.
|
|
90
|
+
`Parse::Agent.new(impersonate_user:, impersonate_mint:, impersonation_label:)`
|
|
91
|
+
and `agent.impersonate(user)` / `agent.stop_impersonating!` resolve a real
|
|
92
|
+
session token for the target `_User` (reusing an active `_Session`, or
|
|
93
|
+
minting a restricted one with `impersonate_mint: true`) and bind it as if
|
|
94
|
+
`session_token:` had been passed. Fails closed: requires a master-key client,
|
|
95
|
+
rejects non-`_User` pointers, and refuses rather than widening to master-key
|
|
96
|
+
posture when no session resolves. An `impersonation_label:` audit tag (also
|
|
97
|
+
usable with `acl_role:`) surfaces on the `parse.agent.tool_call` payload.
|
|
98
|
+
Session lookups run through the agent's own client (not the process-default
|
|
99
|
+
`Parse.client`) so impersonation is correct under multi-client setups, and
|
|
100
|
+
`impersonated_user_id` is stamped only after a token resolves, so a failed
|
|
101
|
+
`impersonate` leaves the agent's identity unchanged. (`lib/parse/agent.rb`)
|
|
102
|
+
- **SECURITY**: aggregate tools (`aggregate`, `group_by`, `distinct`, and the
|
|
103
|
+
pipeline export path) route through the SDK's ACL-enforcing mongo-direct path
|
|
104
|
+
for ANY non-master identity — session-token agents and runtime-impersonated
|
|
105
|
+
agents included, not only `acl_user:` / `acl_role:`. Parse Server's REST
|
|
106
|
+
aggregate endpoint enforces no per-row ACL, so this closes the gap where a
|
|
107
|
+
scoped agent whose ACL context wasn't eagerly resolved could otherwise reach
|
|
108
|
+
the unenforced REST path. (`lib/parse/agent.rb`, `lib/parse/agent/tools.rb`)
|
|
109
|
+
- **NEW**: `Parse::Agent::PromptHardening` — `sanitize_schema_for_llm` (drops
|
|
110
|
+
non-identifier field names, strips control/zero-width chars, caps and
|
|
111
|
+
marker-wraps descriptions) hooked into `get_schema`/`get_all_schemas`;
|
|
112
|
+
`scrub_marker_injection` neutralizing embedded wrapper markers in untrusted
|
|
113
|
+
tool content (`Parse::Agent.prompt_marker_strict` to refuse instead);
|
|
114
|
+
operator-curated `Parse::Agent.prompt_injection_canaries` that emit
|
|
115
|
+
`parse.agent.prompt_injection_detected` (and refuse when
|
|
116
|
+
`canary_action = :refuse`); `Parse::Agent::PROMPT_VERSION` surfaced via
|
|
117
|
+
`agent.describe[:prompt][:version]`; and a one-time warning when
|
|
118
|
+
`allowed_llm_endpoints` is left unrestricted. The `allowed_llm_endpoints`
|
|
119
|
+
allowlist matches on the request's `scheme://host:port` origin (the path is
|
|
120
|
+
ignored), so an entry like `https://api.openai.com` authorizes any path on
|
|
121
|
+
that host but not `https://api.openai.com.evil.com` or
|
|
122
|
+
`…openai.com@evil.com`; a malformed endpoint or entry is a fail-closed miss.
|
|
123
|
+
(`lib/parse/agent/prompt_hardening.rb`)
|
|
124
|
+
- **NEW**: Embedding-cost telemetry on `parse.agent.tool_call` — embedding calls
|
|
125
|
+
made inside a tool span contribute `embed_calls`, `embed_tokens`, and (when
|
|
126
|
+
`Parse::Agent.embed_cost_per_million_tokens` is set) `embed_cost_usd`,
|
|
127
|
+
attributed via a thread-local accumulator fed by the `parse.embeddings.embed`
|
|
128
|
+
notification. (`lib/parse/agent.rb`)
|
|
129
|
+
- **NEW**: Optional per-row `_source` provenance (`{ class, tool, object_id }`)
|
|
130
|
+
on read-tool results, enabled with `Parse::Agent.include_source_provenance`
|
|
131
|
+
(default off). Stamped after field-allowlist projection and redaction across
|
|
132
|
+
`query_class`, `get_objects`, `aggregate`, `atlas_text_search`, and
|
|
133
|
+
`semantic_search`. (`lib/parse/agent/tools.rb`, `lib/parse/retrieval/agent_tool.rb`)
|
|
134
|
+
- **NEW**: General-purpose server-initiated notification stream.
|
|
135
|
+
`MCPRackApp.new(notifications: true)` opens the GET listening-stream bus
|
|
136
|
+
without enabling LiveQuery resource subscriptions, and
|
|
137
|
+
`MCPRackApp#notify(session_id, method:, params:)` pushes arbitrary
|
|
138
|
+
`notifications/*` events to a session. (`lib/parse/agent/mcp_rack_app.rb`)
|
|
139
|
+
- **SECURITY**: the MCP listening stream is now owner-bound. A session
|
|
140
|
+
established through `initialize` is bound to that caller's principal, and
|
|
141
|
+
only the same principal may later open its server→client SSE stream; a
|
|
142
|
+
different authenticated caller who knows or guesses the `Mcp-Session-Id` is
|
|
143
|
+
refused with `403`. An id never seen by `initialize` (the decoupled
|
|
144
|
+
`notifications:` bus) is claimed trust-on-first-use by the first principal to
|
|
145
|
+
attach, so a second principal can no longer evict or shadow an existing
|
|
146
|
+
listener. The principal is derived from the agent's scope (session_token →
|
|
147
|
+
acl_user → acl_role); a new `MCPRackApp.new(principal_resolver:)` callable
|
|
148
|
+
lets a master-key deployment that authenticates users upstream supply a real
|
|
149
|
+
per-user principal (without it, bare master-key agents share one principal
|
|
150
|
+
and owner-binding is a no-op among them). The binding registry is
|
|
151
|
+
per-process and does not span Puma workers or survive restart — the same
|
|
152
|
+
scope as the cancellation registry — so in a cluster the `initialize`
|
|
153
|
+
binding degrades to trust-on-first-use. The GET stream must carry the same
|
|
154
|
+
credential as `initialize`. (`lib/parse/agent/mcp_rack_app.rb`)
|
|
155
|
+
- **IMPROVED**: operator fail-loud lints for two silent misconfigurations.
|
|
156
|
+
(1) A `:write`/`:admin` agent served over MCP with `Parse::Agent.require_approval_for`
|
|
157
|
+
empty emits a one-time `[Parse::Agent:SECURITY]` warning (every write runs
|
|
158
|
+
ungated). (2) When any class declares `agent_tenant_scope`, a class explicitly
|
|
159
|
+
exposed to agents (via `agent_fields` or `agent_searchable`) that declares none
|
|
160
|
+
emits a one-time per-class warning on the query path (the search path already
|
|
161
|
+
raises `MissingTenantScope`) — surfacing the silent cross-tenant pass-through
|
|
162
|
+
instead of leaving it to leaked rows. The warning is gated to agent-exposed
|
|
163
|
+
classes so system/incidental classes a tool merely touches don't create noise.
|
|
164
|
+
(`lib/parse/agent/mcp_rack_app.rb`, `lib/parse/agent/metadata_registry.rb`)
|
|
165
|
+
- **NEW**: MCP approval round-trips emit a `parse.agent.approval`
|
|
166
|
+
`ActiveSupport::Notifications` event carrying `tool`, `effective_permission`,
|
|
167
|
+
`correlation_id`, `timeout`, `outcome`, and `reason`, with the measured wait
|
|
168
|
+
as the event duration — so a non-answering client holding a dispatcher thread
|
|
169
|
+
for the full `approval_timeout` is observable. (`lib/parse/agent/approval_gate.rb`)
|
|
170
|
+
|
|
171
|
+
#### Token economy — leaner tool surface, responses, and retrieval
|
|
172
|
+
|
|
173
|
+
The MCP surface is paid for in LLM context tokens. This batch cuts the fixed
|
|
174
|
+
per-session tool tax, trims per-row response overhead, and guards retrieval
|
|
175
|
+
against silent context blowout.
|
|
176
|
+
|
|
177
|
+
- **NEW**: `Parse::Agent.new(tools: :lean)` — a named tool-surface profile that
|
|
178
|
+
narrows `:readonly` to the six core read tools (`get_all_schemas`,
|
|
179
|
+
`get_schema`, `query_class`, `count_objects`, `get_object`, `aggregate`),
|
|
180
|
+
taking a full `tools/list` payload from ~7.9K context tokens to ~2.6K (~67%).
|
|
181
|
+
Profiles (`Parse::Agent::TOOL_PROFILES`) are Symbol-only, compose with the
|
|
182
|
+
permission tier (narrow-only), and raise on an unknown name rather than
|
|
183
|
+
silently exposing the full surface. (`lib/parse/agent.rb`)
|
|
184
|
+
- **IMPROVED**: read-tool responses now strip the raw `ACL` map before reaching
|
|
185
|
+
the model — it is operationally useless to a model (effective authority is
|
|
186
|
+
enforced server-side) and is pure token overhead plus a minor role/user-id
|
|
187
|
+
disclosure. (`lib/parse/agent/result_formatter.rb`)
|
|
188
|
+
- **FIXED**: `get_objects` and the Atlas Search tools now normalize rows through
|
|
189
|
+
the same `simplify_object` form `query_class` uses (compact pointers, ISO
|
|
190
|
+
dates, ACL stripped) instead of shipping raw wire-form with `__type` dicts.
|
|
191
|
+
(`lib/parse/agent/tools.rb`)
|
|
192
|
+
- **CHANGED**: the `semantic_search` result hoists each chunk's parent record
|
|
193
|
+
**once** into a `documents` map keyed by `objectId` instead of duplicating the
|
|
194
|
+
full source on every chunk — map a chunk to its source via
|
|
195
|
+
`metadata.object_id`. Saves the repeated-source cost on every multi-chunk
|
|
196
|
+
document. (`lib/parse/retrieval/agent_tool.rb`)
|
|
197
|
+
- **NEW**: `semantic_search` `max_total_tokens:` budget (default 20,000;
|
|
198
|
+
estimated chars/4) trims the lowest-ranked chunks so a few long documents
|
|
199
|
+
can't silently blow the context window, adding `budget_truncated: true` /
|
|
200
|
+
`budget_dropped: <n>` when it trims. Pass `0` to disable.
|
|
201
|
+
(`lib/parse/retrieval/agent_tool.rb`)
|
|
202
|
+
- **IMPROVED**: a failing `tools/call` now forwards its structured
|
|
203
|
+
`error_code` / `retry_after` / `details` on the MCP error envelope under
|
|
204
|
+
`_meta` (`parse.error_code`, `parse.retry_after`, `parse.details`) so clients
|
|
205
|
+
can branch deterministically and honor `retry_after` without parsing prose.
|
|
206
|
+
(`lib/parse/agent/mcp_dispatcher.rb`)
|
|
207
|
+
- **IMPROVED**: `get_schema` on a mistyped class name raises a `ValidationError`
|
|
208
|
+
carrying a "Did you mean: …?" hint (near matches from locally-known classes),
|
|
209
|
+
letting the model self-correct in one retry instead of a full
|
|
210
|
+
`get_all_schemas` sweep. (`lib/parse/agent/tools.rb`)
|
|
211
|
+
- **NEW**: `Parse::Agent.measure_embeddings { … }` scopes embedding usage
|
|
212
|
+
(`{ calls:, tokens:, cost_usd: }`) around arbitrary work on the calling
|
|
213
|
+
thread — capturing corpus/ingestion embeds fired at `Model.save` time that
|
|
214
|
+
the per-tool-call telemetry does not span. `Parse::Agent.embed_cost_usd(tokens)`
|
|
215
|
+
exposes the rate conversion. (`lib/parse/agent.rb`)
|
|
216
|
+
|
|
217
|
+
#### Expanded integration test coverage
|
|
218
|
+
|
|
219
|
+
- **IMPROVED**: Added end-to-end coverage for cloud-function error scenarios —
|
|
220
|
+
a bare cloud throw, a typed `Parse.Error`, and an application-defined error
|
|
221
|
+
code all surface as `Parse::Error::CloudCodeError` with the wire code,
|
|
222
|
+
message, and HTTP status preserved, and a `beforeSave` validation rejection
|
|
223
|
+
propagates to the object save path.
|
|
224
|
+
(`test/lib/parse/cloud_function_errors_integration_test.rb`)
|
|
225
|
+
- **IMPROVED**: Added "disruptive" integration tests that exercise a real Parse
|
|
226
|
+
Server outage and restart against a live container: connectivity predicates
|
|
227
|
+
flip to false and recover, in-flight requests raise a connection-class error,
|
|
228
|
+
and a registered webhook survives a server restart with idempotent
|
|
229
|
+
re-registration. Run via `rake test:integration:disruptive` so they never
|
|
230
|
+
interleave with the rest of the suite.
|
|
231
|
+
(`test/lib/parse/network_failure_disruptive_test.rb`,
|
|
232
|
+
`test/lib/parse/webhook_restart_disruptive_test.rb`)
|
|
233
|
+
|
|
234
|
+
#### `unique_index_on` — declarative correctness floor for `first_or_create!`
|
|
235
|
+
|
|
236
|
+
`Parse::Object` subclasses can now declare the MongoDB unique index that backs
|
|
237
|
+
the `first_or_create!` / `create_or_update!` race fix directly on the model,
|
|
238
|
+
with intent-revealing syntax. The Redis-backed `synchronize:` lock is a latency
|
|
239
|
+
optimization that collapses concurrent callers in the common path; the unique
|
|
240
|
+
index is the correctness floor that holds when the lock is bypassed — a Redis
|
|
241
|
+
outage, a TTL expiring mid-write, or a `synchronize: false` caller. With the
|
|
242
|
+
index in place a duplicate insert surfaces as Parse error 137 (DuplicateValue),
|
|
243
|
+
which `first_or_create!` already rescues and resolves to the winning row, so the
|
|
244
|
+
net invariant — exactly one row, every caller sees the same id — holds under any
|
|
245
|
+
race.
|
|
246
|
+
|
|
247
|
+
- **NEW**: `unique_index_on(*fields, sparse: false, partial: nil, name: nil)`
|
|
248
|
+
declares a unique index on the exact dedup tuple. It is thin sugar over
|
|
249
|
+
`mongo_index(*fields, unique: true, …)`, sharing the same registration,
|
|
250
|
+
validation (sensitive-field guard, pointer auto-rewrite to `_p_<field>`,
|
|
251
|
+
parallel-array / relation / `_id` rejection), and `apply_indexes!` writer
|
|
252
|
+
path. (`lib/parse/model/core/indexing.rb`)
|
|
253
|
+
- **NEW**: The default is non-sparse, keeping the index key identical to the
|
|
254
|
+
query `first_or_create!` re-runs on recovery so a 137 always maps to a row the
|
|
255
|
+
recovery query can find. `partial:` is the documented escape hatch for
|
|
256
|
+
"unique within a subset" (e.g. unique email per tenant, where tenant-less rows
|
|
257
|
+
may repeat); `sparse:` only changes behavior for rows missing the entire
|
|
258
|
+
tuple, which the create path never produces.
|
|
259
|
+
|
|
260
|
+
#### MCP resource subscriptions bridged to LiveQuery
|
|
261
|
+
|
|
262
|
+
The MCP server can now serve `resources/subscribe` and push
|
|
263
|
+
`notifications/resources/updated` over a server→client channel, backed by
|
|
264
|
+
Parse LiveQuery. A client subscribes to a class's `count` or `samples`
|
|
265
|
+
resource; when the underlying data changes, the server emits one coarse
|
|
266
|
+
update for that URI and the client re-reads the resource. Disabled by default
|
|
267
|
+
and only advertised when LiveQuery is enabled and available, so the
|
|
268
|
+
`resources.subscribe` capability is never claimed unless the server can
|
|
269
|
+
actually deliver.
|
|
270
|
+
|
|
271
|
+
- **NEW**: `Parse::Agent::MCPRackApp.new(resource_subscriptions: true)` accepts
|
|
272
|
+
a long-lived `GET` (`Accept: text/event-stream`, `Mcp-Session-Id`) as the
|
|
273
|
+
listening stream that carries `notifications/resources/updated`, and routes
|
|
274
|
+
`resources/subscribe` / `resources/unsubscribe` to a LiveQuery bridge.
|
|
275
|
+
Requires a streaming-capable Rack server (Puma, Falcon) and
|
|
276
|
+
`Parse.live_query_enabled = true`. (`lib/parse/agent/mcp_rack_app.rb`)
|
|
277
|
+
- **NEW**: `Parse::Agent::MCPSubscriptions::Manager` coordinates per-session
|
|
278
|
+
subscriptions, derives LiveQuery credentials from the subscribing agent,
|
|
279
|
+
debounces bursts per `(session, uri)` into a single update, and tears down
|
|
280
|
+
LiveQuery subscriptions when the listening stream closes or the session is
|
|
281
|
+
terminated via `DELETE`. (`lib/parse/agent/mcp_subscriptions.rb`)
|
|
282
|
+
- **NEW**: `resources.subscribe` is advertised on `initialize` only when a
|
|
283
|
+
supported subscription manager is wired; the WEBrick `MCPServer` and the
|
|
284
|
+
Rack app with subscriptions disabled continue to advertise `subscribe:
|
|
285
|
+
false`. (`lib/parse/agent/mcp_dispatcher.rb`)
|
|
286
|
+
- **SECURITY**: the LiveQuery bridge enforces the SDK's scope asymmetry —
|
|
287
|
+
session-token agents subscribe with their token (Parse Server filters events
|
|
288
|
+
to readable rows), master-key agents subscribe over a dedicated admin
|
|
289
|
+
LiveQuery connection, and `acl_user:` / `acl_role:` agents are refused
|
|
290
|
+
because Parse Server LiveQuery has no act-as-user / act-as-role handshake.
|
|
291
|
+
Because Parse Server fixes ACL-bypass at connect time (there is no
|
|
292
|
+
per-subscription master key), master- and session-scoped subscriptions are
|
|
293
|
+
routed to separate connections; the bridge fails closed rather than open a
|
|
294
|
+
mis-scoped channel. (`lib/parse/agent/mcp_subscriptions.rb`)
|
|
295
|
+
- **SECURITY**: `resources/subscribe` enforces the same class-authorization gate
|
|
296
|
+
as the read path before opening any socket — `agent_hidden` declarations and
|
|
297
|
+
the per-agent `classes:` allowlist are checked via
|
|
298
|
+
`Tools.assert_class_accessible!`, so a hidden or out-of-allowlist class (e.g.
|
|
299
|
+
`parse://_Session/count`) can no longer become a change/timing oracle through
|
|
300
|
+
a subscription that bypasses the tool surface. A denial opens no socket.
|
|
301
|
+
(`lib/parse/agent/mcp_subscriptions.rb`, `lib/parse/agent/mcp_dispatcher.rb`)
|
|
302
|
+
- **SECURITY**: the master-key subscription branch is bound to the agent's own
|
|
303
|
+
master-key authority. Because the admin LiveQuery client backfills the
|
|
304
|
+
process-global master key, an unprivileged / client-mode agent (no master key
|
|
305
|
+
on its own client) in a no-scope posture is now refused rather than allowed to
|
|
306
|
+
borrow the global key for an ACL-bypassing admin socket. Symmetrically, a
|
|
307
|
+
session-token subscription is refused if the shared scoped LiveQuery client is
|
|
308
|
+
itself an admin connection (`config.use_master_key = true`), so it can never
|
|
309
|
+
ride an ACL-bypassing socket. The subscribe and cap checks are also
|
|
310
|
+
re-validated under the lock after the network subscribe so a torn-down session
|
|
311
|
+
can't be resurrected into a leaked socket. (`lib/parse/agent/mcp_subscriptions.rb`)
|
|
312
|
+
- **NEW**: only `count` and `samples` resources are subscribable; `schema` is
|
|
313
|
+
rejected because schema changes are not LiveQuery events. A per-session
|
|
314
|
+
subscription cap (default 100) bounds the footprint of a client that
|
|
315
|
+
subscribes but never opens its listening stream.
|
|
316
|
+
|
|
317
|
+
#### Fix wrong pointer/relation `targetClass` for built-in system-class associations
|
|
318
|
+
|
|
319
|
+
A built-in association to a Parse Server system class could freeze the wrong
|
|
320
|
+
`targetClass` into the generated schema. `Parse::Installation`'s
|
|
321
|
+
`belongs_to :user` (and, by the same mechanism, `Parse::Session#user` and
|
|
322
|
+
`Parse::Role#users`) resolved its target through `:user.to_parse_class`,
|
|
323
|
+
which depends on `Parse::User` already being registered. Because the gem
|
|
324
|
+
loads the built-in classes before `user.rb`, the lookup fell back to the
|
|
325
|
+
camelized literal `"User"` instead of the Parse storage name `"_User"` and
|
|
326
|
+
stored it in the association `references` / `relations` map. That literal was
|
|
327
|
+
then emitted as the pointer column's `targetClass`, so a fresh schema push
|
|
328
|
+
created `_Installation.user` with `targetClass: "User"` — and Parse Server
|
|
329
|
+
rejected every `_User` pointer save against it (`expected Pointer<User> but
|
|
330
|
+
got Pointer<_User>`). This is the root cause underlying the spurious
|
|
331
|
+
className-mismatch warnings addressed cosmetically in 5.1.1.
|
|
332
|
+
|
|
333
|
+
- **FIXED**: `belongs_to` / `has_many` / `has_one` associations to a Parse
|
|
334
|
+
Server system class (`User`, `Role`, `Session`, `Installation`, and the
|
|
335
|
+
other built-ins) now resolve to the correct leading-underscore storage name
|
|
336
|
+
regardless of class load order. `Parse::Installation.references[:user]`,
|
|
337
|
+
`Parse::Session.references[:user]`, and `Parse::Role.relations[:users]` now
|
|
338
|
+
hold `"_User"`, so the emitted schema pointer/relation `targetClass` is
|
|
339
|
+
`"_User"` and `_User` pointer saves are accepted.
|
|
340
|
+
(`lib/parse/model/model.rb`)
|
|
341
|
+
- **FIXED**: `String#to_parse_class` / `Symbol#to_parse_class` now resolve a
|
|
342
|
+
built-in system class by name even when its Ruby class is not yet
|
|
343
|
+
registered, restoring the documented contract
|
|
344
|
+
(`"users".to_parse_class(singularize: true) # => "_User"`). A registered
|
|
345
|
+
class with a custom `parse_class` table mapping still takes precedence, so
|
|
346
|
+
application classes are never rewritten. (`lib/parse/model/model.rb`)
|
|
347
|
+
- **NEW**: `Parse::Model::SYSTEM_CLASS_MAP` maps each built-in's camelized
|
|
348
|
+
bare name to its storage name; it is consulted by `to_parse_class` only as
|
|
349
|
+
a fallback when class resolution would otherwise fail. (`lib/parse/model/model.rb`)
|
|
350
|
+
- **CHANGED**: The one-time `_Installation` CLP advisory is now
|
|
351
|
+
operation-aware. `set_clp` and `set_class_access` warn only for the
|
|
352
|
+
operations Parse Server ignores on `_Installation` (`find`, `create`,
|
|
353
|
+
`update`, `delete`); configuring the operations it honors (`get`, `count`,
|
|
354
|
+
`addField`) no longer emits a warning. The pointer-permission helpers
|
|
355
|
+
`set_read_user_fields` / `set_write_user_fields` still warn, since they have
|
|
356
|
+
no reliable owner identity to bind to on `_Installation`.
|
|
357
|
+
(`lib/parse/model/classes/installation.rb`)
|
|
358
|
+
|
|
359
|
+
This corrects schema generation going forward. An environment whose schema was
|
|
360
|
+
already pushed with the wrong `targetClass` must have that schema re-pushed
|
|
361
|
+
(for example by re-running the schema upgrade) after upgrading; an existing
|
|
362
|
+
pointer column's `targetClass` cannot be altered in place and must be replaced.
|
|
363
|
+
|
|
364
|
+
#### Harden `$relatedTo` against cross-class access on agent and mongo-direct paths
|
|
365
|
+
|
|
366
|
+
The `$relatedTo` query operator reaches across to a second class — the owning
|
|
367
|
+
object whose relation is being read — but, unlike the other cross-class
|
|
368
|
+
operators (`$inQuery` / `$notInQuery` / `$select` / `$dontSelect`), the class
|
|
369
|
+
named by its `object` pointer was not run through the agent accessibility
|
|
370
|
+
policy. An agent narrowed by `classes:` (or with a class declared
|
|
371
|
+
`agent_hidden`) could therefore name a relation anchored on a class outside its
|
|
372
|
+
allowlist. The same operator on the mongo-direct path was passed through to
|
|
373
|
+
MongoDB verbatim, where it failed with an opaque "unknown operator" error.
|
|
374
|
+
|
|
375
|
+
- **FIXED**: `Parse::Agent::ConstraintTranslator` now validates the owning-object
|
|
376
|
+
class of a `$relatedTo` constraint against the agent's accessibility policy,
|
|
377
|
+
matching how `$inQuery` / `$select` are already gated. The check fails closed
|
|
378
|
+
when the owning class cannot be resolved from the `object` pointer.
|
|
379
|
+
(`lib/parse/agent/constraint_translator.rb`)
|
|
380
|
+
- **FIXED**: the constraint translator now classifies each key of a constraint
|
|
381
|
+
hash independently instead of only validating operators when *every* key in
|
|
382
|
+
the hash is an operator. An operator sharing a hash with a non-operator field
|
|
383
|
+
key — reachable as a `$or` / `$and` / `$nor` array element — was previously
|
|
384
|
+
routed to the field branch and skipped operator validation, so a blocked
|
|
385
|
+
operator (e.g. `$where`) or an off-allowlist cross-class / relation reference
|
|
386
|
+
could pass through alongside a throwaway field key. Operators are now
|
|
387
|
+
validated and dispatched at every nesting level regardless of sibling keys.
|
|
388
|
+
(`lib/parse/agent/constraint_translator.rb`)
|
|
389
|
+
- **FIXED**: `$relatedTo` on the mongo-direct query path
|
|
390
|
+
(`results_direct` / `count_direct` / `distinct_direct`) now raises a clear
|
|
391
|
+
`ArgumentError` explaining that Parse Relations are resolved server-side, so
|
|
392
|
+
the query must run via REST. This replaces the previous opaque MongoDB error
|
|
393
|
+
and forecloses a future `$lookup` rewrite that could bypass the row-level
|
|
394
|
+
`_rperm` / `protectedFields` enforcement the rest of that path applies.
|
|
395
|
+
(`lib/parse/query.rb`)
|
|
396
|
+
|
|
397
|
+
#### Schema migration — correct wire-column names and a one-way convergence check
|
|
398
|
+
|
|
399
|
+
The migration tooling derived every wire column name with `camelize(:lower)`,
|
|
400
|
+
which ignored custom `field:` mappings and double-counted multi-word
|
|
401
|
+
properties (each property is registered under both its snake_case key and its
|
|
402
|
+
camelCase wire alias). Wire names are now resolved through the model's
|
|
403
|
+
`field_map`, so each column is emitted exactly once and custom mappings are
|
|
404
|
+
honored.
|
|
405
|
+
|
|
406
|
+
- **FIXED**: `Parse::Schema.migration(Klass).preview` / `.operations` no longer
|
|
407
|
+
list multi-word fields twice (e.g. `ADD FIELD unitPrice` appeared once per
|
|
408
|
+
alias). Each declared property now produces exactly one operation.
|
|
409
|
+
(`lib/parse/schema.rb`)
|
|
410
|
+
- **FIXED**: `auto_upgrade!` and `Migration#apply!` now create the column a
|
|
411
|
+
property actually serializes to. For `property :unit_price, field: "price_usd"`
|
|
412
|
+
the migrator previously created a phantom `unitPrice` (or `priceUsd`) column
|
|
413
|
+
instead of the declared `price_usd`; it now creates `price_usd`. Pointer
|
|
414
|
+
columns are likewise emitted at their true wire name. (`lib/parse/schema.rb`)
|
|
415
|
+
- **FIXED**: `SchemaDiff#missing_on_server` is keyed by the canonical property
|
|
416
|
+
name with no camelCase alias duplicates.
|
|
417
|
+
- **NEW**: `Parse::Schema::SchemaDiff#server_covers_local?` — a one-way
|
|
418
|
+
convergence check (`missing_on_server.empty? && type_mismatches.empty?`) for
|
|
419
|
+
CI pipelines. Unlike `in_sync?` (which is strict and bidirectional, so it
|
|
420
|
+
reports `false` whenever the server has columns the model does not declare —
|
|
421
|
+
e.g. a dashboard-added field), `server_covers_local?` answers the question a
|
|
422
|
+
deploy gate actually asks: "is every field my model declares present on the
|
|
423
|
+
server?" (`lib/parse/schema.rb`)
|
|
424
|
+
- **FIXED**: `Migration#needed?` is now defined in terms of
|
|
425
|
+
`server_covers_local?`, so a server that is a superset of the model no longer
|
|
426
|
+
produces a "needed" migration with zero operations.
|
|
427
|
+
|
|
428
|
+
#### Aggregation — `group_by` class methods and operation-aware table headers
|
|
429
|
+
|
|
430
|
+
- **NEW**: `Klass.group_by` and `Klass.group_by_date` class-method delegators,
|
|
431
|
+
mirroring the existing `Klass.distinct` / `Klass.count_distinct` delegation.
|
|
432
|
+
`Post.group_by(:category).count` now works without the explicit
|
|
433
|
+
`Post.query.group_by(...)` form. (`lib/parse/model/core/querying.rb`)
|
|
434
|
+
- **FIXED**: `Parse::GroupedResult#to_table` derives the value-column header
|
|
435
|
+
from the aggregation operation (`Average`, `Sum`, `Min`, `Max`, …) instead of
|
|
436
|
+
always printing `Count`, so an averaged table is no longer mislabeled. An
|
|
437
|
+
explicit `headers:` override still takes precedence, and results constructed
|
|
438
|
+
without an operation continue to default to `Count`. (`lib/parse/query.rb`)
|
|
439
|
+
|
|
440
|
+
#### Connectivity probes — `Parse.connected?` / `Parse.reachable?`
|
|
441
|
+
|
|
442
|
+
- **NEW**: `Parse.reachable?` / `Parse::Client#reachable?` — a no-credentials
|
|
443
|
+
liveness probe that hits the server health endpoint; passes whenever the
|
|
444
|
+
server is up, even if the configured `application_id` / REST key is wrong.
|
|
445
|
+
- **NEW**: `Parse.connected?` / `Parse::Client#connected?` — a connectivity
|
|
446
|
+
probe that by default hits the health endpoint, so it returns `true` whenever
|
|
447
|
+
the server is up, regardless of Class-Level-Permission configuration. (A
|
|
448
|
+
`_User` find is unreliable as the default probe: locking `_User` finds to the
|
|
449
|
+
master key via CLP is standard production hardening and would make the probe
|
|
450
|
+
report "not connected" on a perfectly healthy, correctly-configured server.)
|
|
451
|
+
Pass an endpoint — `connected?("classes/_User")`, or
|
|
452
|
+
`Parse.connected?(:default, "classes/_User")` — to additionally validate
|
|
453
|
+
credentials against a class the configured key can read; the probe runs
|
|
454
|
+
`limit: 0` (never pulls rows) and routes through the auth stack, so a wrong
|
|
455
|
+
`application_id` / REST key returns `false`. Connection failures, timeouts,
|
|
456
|
+
and API errors all return `false` rather than raising; genuine programming
|
|
457
|
+
errors still propagate. (`lib/parse/client.rb`)
|
|
458
|
+
|
|
459
|
+
#### `request_password_reset` — documented failure mode
|
|
460
|
+
|
|
461
|
+
- **IMPROVED**: `Parse::User.request_password_reset` (and the instance form)
|
|
462
|
+
now document that they raise `Parse::Error::ServiceUnavailableError` when the
|
|
463
|
+
server returns 500/503 (for example, when no email adapter is configured), so
|
|
464
|
+
callers branching on the documented Boolean return know to rescue it.
|
|
465
|
+
(`lib/parse/model/classes/user.rb`)
|
|
466
|
+
|
|
467
|
+
#### `Parse::Client#request` — bounded, idempotency-aware retries on 500/503/429
|
|
468
|
+
|
|
469
|
+
- **FIXED**: a request that received a persistent `500`/`503`
|
|
470
|
+
(`ServiceUnavailableError`) or `429` (`RequestLimitExceededError`) retried
|
|
471
|
+
**forever** instead of giving up after `retry_limit` attempts. The retry path
|
|
472
|
+
uses Ruby's `retry` keyword, which re-runs the method body; because the retry
|
|
473
|
+
counter was initialized inside that re-run, every attempt reset it back to
|
|
474
|
+
`retry_limit`. The counter is now initialized once, above the retried block,
|
|
475
|
+
so the countdown is preserved — a persistent failure makes exactly
|
|
476
|
+
`retry_limit + 1` attempts and then raises. An explicit `opts[:retry]` count
|
|
477
|
+
is likewise honored and bounded. (`lib/parse/client.rb`)
|
|
478
|
+
- **FIXED**: the backoff delay collapsed to zero (or negative, so no
|
|
479
|
+
sleep at all) whenever a caller passed `opts: { retry: N }` with `N` above the
|
|
480
|
+
client's `retry_limit`, because the backoff multiplier was derived from
|
|
481
|
+
`retry_limit` rather than the effective starting budget. The multiplier now
|
|
482
|
+
uses the actual starting count, so every retry backs off by a strictly
|
|
483
|
+
positive, growing delay. (Backoff is linear in the attempt number —
|
|
484
|
+
`RETRY_DELAY × attempt` — with ±25% jitter.) (`lib/parse/client.rb`)
|
|
485
|
+
- **CHANGED**: retries are now idempotency-aware, so a transient server error
|
|
486
|
+
can't double-apply a write. A `429` re-sends regardless of method (the server
|
|
487
|
+
provably discarded the request). For an ambiguous failure — a `500`/`503` or a
|
|
488
|
+
dropped connection, where the write may already have applied — only idempotent
|
|
489
|
+
requests are re-sent: `GET` and `DELETE` always, a `PUT` update only when its
|
|
490
|
+
body carries no atomic operation (`Increment` / `Add` / `AddUnique` /
|
|
491
|
+
`Remove` / relation ops), and `POST` (object create / batch) never.
|
|
492
|
+
(`lib/parse/client.rb`)
|
|
493
|
+
- **FIXED**: a request that hit a read timeout was not retried. Faraday 2.x
|
|
494
|
+
raises `Faraday::TimeoutError` (a `Faraday::Error`, not a `Faraday::ClientError`)
|
|
495
|
+
for `Timeout::Error` / `Errno::ETIMEDOUT`, which the retry clause didn't list,
|
|
496
|
+
so a timed-out request propagated raw instead of being retried (when
|
|
497
|
+
idempotent) and wrapped as `Parse::Error::ConnectionError`. The clause now
|
|
498
|
+
catches `Faraday::TimeoutError`. Connection-*refused* (`Faraday::ConnectionFailed`)
|
|
499
|
+
is intentionally still not retried — it is a non-transient failure and
|
|
500
|
+
retrying it only adds backoff latency. (`lib/parse/client.rb`)
|
|
501
|
+
- **NEW**: `Parse::Request.assume_server_idempotency` — an explicit opt-in that
|
|
502
|
+
makes writes retry-safe end-to-end on ambiguous failures. The SDK already
|
|
503
|
+
sends a stable `X-Parse-Request-Id` (`_RB_<uuid>`) on POST/PUT/PATCH by
|
|
504
|
+
default (`Parse::Request.enable_request_id`), and now reuses the SAME id on
|
|
505
|
+
every retry attempt, so when Parse Server is configured with
|
|
506
|
+
`idempotencyOptions` covering the targeted paths, a replayed write is
|
|
507
|
+
deduplicated server-side — the second delivery never creates a duplicate.
|
|
508
|
+
With this flag set (default `false`, also settable via
|
|
509
|
+
`enable_idempotency!(assume_server_dedup: true)` / `configure_idempotency`),
|
|
510
|
+
the retry guard treats any request carrying a request-id header as retry-safe
|
|
511
|
+
regardless of method — so a `POST` create or an atomic-op `PUT` that hits a
|
|
512
|
+
`500`/`503` or a request timeout is transparently retried. Left off, behavior
|
|
513
|
+
is unchanged (the client never assumes the server dedups). On the rare
|
|
514
|
+
ambiguous-success case (the first attempt landed but its response was lost),
|
|
515
|
+
Parse Server answers the replay with a `Duplicate request` (Parse code `159`),
|
|
516
|
+
which the SDK now surfaces as a typed, catchable `Parse::Error::DuplicateRequestError`
|
|
517
|
+
meaning "the original write already applied" — re-fetch by your own key if you
|
|
518
|
+
need the resulting object (Parse Server does not echo the original response on
|
|
519
|
+
a duplicate). Proven end-to-end by
|
|
520
|
+
`test/lib/parse/client/idempotent_retry_integration_test.rb` against a test
|
|
521
|
+
stack configured with path-scoped server idempotency.
|
|
522
|
+
(`lib/parse/client/request.rb`, `lib/parse/client.rb`)
|
|
523
|
+
- **CHANGED**: two error-surface changes above are behavioral, not just
|
|
524
|
+
additive — review downstream `rescue` chains when upgrading. (1) Parse code
|
|
525
|
+
`159` now raises `Parse::Error::DuplicateRequestError`; previously it fell
|
|
526
|
+
through and returned a response with `success? == false`. Callers that branch
|
|
527
|
+
on `response.success?` / `response.error` instead of rescuing will now see an
|
|
528
|
+
exception — rescue `DuplicateRequestError` and treat it as "already applied."
|
|
529
|
+
(2) A read timeout now raises `Parse::Error::ConnectionError` (after an
|
|
530
|
+
idempotency-gated retry); previously `Faraday::TimeoutError` propagated raw,
|
|
531
|
+
so any `rescue Faraday::TimeoutError` becomes dead code. (`lib/parse/client.rb`)
|
|
532
|
+
- **NEW**: `Parse::Error::DuplicateRequestError` (Parse code `159`,
|
|
533
|
+
`Parse::Response::ERROR_DUPLICATE_REQUEST`) — raised when Parse Server's
|
|
534
|
+
request-id idempotency layer rejects a duplicate `X-Parse-Request-Id`. The
|
|
535
|
+
duplicate is not applied a second time; the original request already
|
|
536
|
+
succeeded. (`lib/parse/client.rb`, `lib/parse/client/response.rb`)
|
|
537
|
+
- **IMPROVED**: `first_or_create!` and `create_or_update!` now recover
|
|
538
|
+
transparently from a `DuplicateRequestError`. These methods already carry the
|
|
539
|
+
identifying `query_attrs`, so when a transparently-retried create lands but
|
|
540
|
+
loses its response (and the replay is rejected with 159), they re-find the row
|
|
541
|
+
the original attempt created and return it — turning the duplicate into a
|
|
542
|
+
successful find instead of a raised error. Applies on both the synchronized
|
|
543
|
+
(`synchronize:`) and unsynchronized paths, and composes with the existing
|
|
544
|
+
duplicate-*value* (unique-index) recovery. A plain `save!` still surfaces
|
|
545
|
+
`DuplicateRequestError` (a generic create has no natural key to re-fetch by;
|
|
546
|
+
catch it and re-query your own unique field). (`lib/parse/model/core/actions.rb`)
|
|
547
|
+
|
|
3
548
|
### 5.1.1
|
|
4
549
|
|
|
5
550
|
#### Suppress spurious className-mismatch warnings for system-class underscore aliases
|
data/Gemfile
CHANGED
|
@@ -12,6 +12,9 @@ group :test, :development do
|
|
|
12
12
|
gem "minitest-mock"
|
|
13
13
|
gem 'minitest-reporters'
|
|
14
14
|
gem "pry"
|
|
15
|
+
# bundler-audit: scans Gemfile.lock against the ruby-advisory-db for known
|
|
16
|
+
# CVEs. Used by the upstream-watch skill and dependency review.
|
|
17
|
+
gem "bundler-audit", ">= 0.9"
|
|
15
18
|
gem "yard", ">= 0.9.11"
|
|
16
19
|
# Rack 3 removed Rack::Server (used by `yard server`); the rackup gem
|
|
17
20
|
# restores it. Drop this once YARD's server adapter stops referencing it.
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
parse-stack-next (5.
|
|
4
|
+
parse-stack-next (5.2.0)
|
|
5
5
|
activemodel (>= 6.1, < 9)
|
|
6
6
|
activesupport (>= 6.1, < 9)
|
|
7
7
|
connection_pool (>= 2.2, < 4)
|
|
@@ -36,6 +36,9 @@ GEM
|
|
|
36
36
|
bigdecimal (4.1.2)
|
|
37
37
|
bson (5.2.0)
|
|
38
38
|
builder (3.3.0)
|
|
39
|
+
bundler-audit (0.9.3)
|
|
40
|
+
bundler (>= 1.2.0)
|
|
41
|
+
thor (~> 1.0)
|
|
39
42
|
coderay (1.1.3)
|
|
40
43
|
concurrent-ruby (1.3.6)
|
|
41
44
|
connection_pool (3.0.2)
|
|
@@ -141,6 +144,7 @@ GEM
|
|
|
141
144
|
rack-session (>= 2.0.0, < 3)
|
|
142
145
|
tilt (~> 2.0)
|
|
143
146
|
stringio (3.2.0)
|
|
147
|
+
thor (1.5.0)
|
|
144
148
|
tilt (2.7.0)
|
|
145
149
|
tsort (0.2.0)
|
|
146
150
|
tzinfo (2.0.6)
|
|
@@ -161,6 +165,7 @@ PLATFORMS
|
|
|
161
165
|
x86_64-linux-musl
|
|
162
166
|
|
|
163
167
|
DEPENDENCIES
|
|
168
|
+
bundler-audit (>= 0.9)
|
|
164
169
|
debug (>= 1.0)
|
|
165
170
|
dotenv
|
|
166
171
|
graphql (~> 2.0)
|