parse-stack-next 4.5.0 → 5.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. checksums.yaml +4 -4
  2. data/.bundle/config +2 -0
  3. data/.env.sample +17 -3
  4. data/.github/workflows/codeql.yml +44 -0
  5. data/.github/workflows/docs.yml +39 -0
  6. data/.github/workflows/release.yml +32 -0
  7. data/.github/workflows/ruby.yml +8 -6
  8. data/.gitignore +4 -0
  9. data/.vscode/settings.json +3 -0
  10. data/CHANGELOG.md +305 -72
  11. data/Gemfile.lock +10 -3
  12. data/LICENSE.txt +1 -1
  13. data/README.md +190 -219
  14. data/Rakefile +1 -1
  15. data/SECURITY.md +30 -0
  16. data/assets/parse-stack-next-avatar.png +0 -0
  17. data/assets/parse-stack-next-avatar.svg +37 -0
  18. data/assets/parse-stack-next-banner.png +0 -0
  19. data/assets/parse-stack-next-banner.svg +45 -0
  20. data/assets/parse-stack-next-social-preview.png +0 -0
  21. data/docs/atlas_vector_search_guide.md +511 -0
  22. data/docs/client_sdk_guide.md +1320 -0
  23. data/docs/mcp_guide.md +225 -104
  24. data/docs/mongodb_direct_guide.md +21 -4
  25. data/docs/usage_guide.md +585 -0
  26. data/examples/transaction_example.rb +28 -28
  27. data/lib/parse/acl_scope.rb +2 -2
  28. data/lib/parse/agent/mcp_rack_app.rb +184 -16
  29. data/lib/parse/agent/metadata_dsl.rb +16 -16
  30. data/lib/parse/agent/pipeline_validator.rb +28 -1
  31. data/lib/parse/agent/prompts.rb +5 -5
  32. data/lib/parse/agent/tools.rb +287 -14
  33. data/lib/parse/agent.rb +209 -12
  34. data/lib/parse/api/analytics.rb +27 -5
  35. data/lib/parse/api/files.rb +6 -2
  36. data/lib/parse/api/push.rb +21 -4
  37. data/lib/parse/api/server.rb +59 -0
  38. data/lib/parse/api/users.rb +26 -2
  39. data/lib/parse/atlas_search/index_manager.rb +84 -0
  40. data/lib/parse/atlas_search.rb +37 -9
  41. data/lib/parse/cache/pool.rb +88 -0
  42. data/lib/parse/cache/redis.rb +249 -0
  43. data/lib/parse/client/body_builder.rb +94 -0
  44. data/lib/parse/client/caching.rb +109 -9
  45. data/lib/parse/client/response.rb +27 -0
  46. data/lib/parse/client.rb +74 -3
  47. data/lib/parse/console.rb +203 -0
  48. data/lib/parse/embeddings/cohere.rb +484 -0
  49. data/lib/parse/embeddings/fixture.rb +130 -0
  50. data/lib/parse/embeddings/jina.rb +454 -0
  51. data/lib/parse/embeddings/local_http.rb +492 -0
  52. data/lib/parse/embeddings/openai.rb +520 -0
  53. data/lib/parse/embeddings/provider.rb +264 -0
  54. data/lib/parse/embeddings/qwen.rb +431 -0
  55. data/lib/parse/embeddings/voyage.rb +550 -0
  56. data/lib/parse/embeddings.rb +225 -0
  57. data/lib/parse/graphql/scalars.rb +53 -0
  58. data/lib/parse/graphql/type_generator.rb +264 -0
  59. data/lib/parse/graphql.rb +48 -0
  60. data/lib/parse/live_query/client.rb +24 -5
  61. data/lib/parse/live_query/subscription.rb +17 -6
  62. data/lib/parse/live_query.rb +9 -4
  63. data/lib/parse/model/associations/collection_proxy.rb +2 -2
  64. data/lib/parse/model/associations/has_many.rb +32 -1
  65. data/lib/parse/model/associations/has_one.rb +17 -0
  66. data/lib/parse/model/associations/pointer_collection_proxy.rb +3 -3
  67. data/lib/parse/model/classes/user.rb +307 -11
  68. data/lib/parse/model/clp.rb +1 -1
  69. data/lib/parse/model/core/create_lock.rb +14 -2
  70. data/lib/parse/model/core/embed_managed.rb +296 -0
  71. data/lib/parse/model/core/fetching.rb +4 -4
  72. data/lib/parse/model/core/indexing.rb +53 -14
  73. data/lib/parse/model/core/parse_reference.rb +3 -3
  74. data/lib/parse/model/core/properties.rb +70 -1
  75. data/lib/parse/model/core/querying.rb +57 -1
  76. data/lib/parse/model/core/vector_searchable.rb +285 -0
  77. data/lib/parse/model/file.rb +16 -4
  78. data/lib/parse/model/model.rb +26 -10
  79. data/lib/parse/model/object.rb +63 -6
  80. data/lib/parse/model/pointer.rb +16 -2
  81. data/lib/parse/model/shortnames.rb +2 -0
  82. data/lib/parse/model/validations/uniqueness_validator.rb +3 -3
  83. data/lib/parse/model/vector.rb +102 -0
  84. data/lib/parse/mongodb.rb +90 -8
  85. data/lib/parse/pipeline_security.rb +59 -2
  86. data/lib/parse/query/constraints.rb +16 -14
  87. data/lib/parse/query/ordering.rb +1 -1
  88. data/lib/parse/query.rb +137 -64
  89. data/lib/parse/stack/generators/templates/model.erb +2 -2
  90. data/lib/parse/stack/generators/templates/model_installation.rb +1 -1
  91. data/lib/parse/stack/generators/templates/model_role.rb +1 -1
  92. data/lib/parse/stack/generators/templates/model_session.rb +1 -1
  93. data/lib/parse/stack/generators/templates/parse.rb +1 -1
  94. data/lib/parse/stack/generators/templates/webhooks.rb +1 -1
  95. data/lib/parse/stack/version.rb +1 -1
  96. data/lib/parse/stack.rb +375 -73
  97. data/lib/parse/two_factor_auth/user_extension.rb +5 -2
  98. data/lib/parse/vector_search.rb +341 -0
  99. data/parse-stack-next.gemspec +10 -9
  100. data/scripts/docker/docker-compose.test.yml +18 -0
  101. data/scripts/start-parse.sh +6 -0
  102. data/scripts/vector_prototype/create_vector_index.js +105 -0
  103. data/scripts/vector_prototype/fetch_embeddings.py +241 -0
  104. data/scripts/vector_prototype/fixture_manifest.json +9 -0
  105. data/scripts/vector_prototype/query_prototype.rb +84 -0
  106. data/scripts/vector_prototype/run.sh +34 -0
  107. metadata +77 -5
  108. data/parse-stack.png +0 -0
data/README.md CHANGED
@@ -1,48 +1,60 @@
1
- > **Note:** This gem is published as **`parse-stack-next`** under the [neurosynq](https://github.com/neurosynq/parse-stack-next) organization. The documentation below was written when the gem was named `parse-stack` — references to `parse-stack` in install instructions, code examples, and links refer to the same codebase. See HEAD on `main` for current docs.
1
+ ![parse_stack_next - The Ruby stack for Parse Server](https://raw.githubusercontent.com/neurosynq/parse-stack-next/main/assets/parse-stack-next-banner.png)
2
2
 
3
- ---
3
+ # Parse Stack Next
4
4
 
5
- ![Parse Stack - The Parse Server Ruby Client SDK](https://raw.githubusercontent.com/modernistik/parse-stack/master/parse-stack.png?raw=true)
5
+ A full-featured Ruby client SDK for [Parse Server](http://parseplatform.org/). [parse-stack-next](https://github.com/neurosynq/parse-stack-next) is a Ruby client SDK, REST client, and Active Model ORM for [Parse Server](http://parseplatform.org/), combining a low-level API client, a query engine, an object-relational mapper (ORM), and a Cloud Code Webhooks rack application in a single gem.
6
6
 
7
- # Parse Stack - Extended Edition
7
+ ### What's new in 5.0
8
8
 
9
- A full featured Active Model ORM and Ruby REST API for Parse-Server. [Parse Stack](https://github.com/neurosynq/parse-stack-next) is the [Parse Server](http://parseplatform.org/) SDK, REST Client and ORM framework for [Ruby](https://www.ruby-lang.org/en/). It provides a client adapter, a query engine, an object relational mapper (ORM) and a Cloud Code Webhooks rack application.
9
+ - **RAG foundation** `:vector` property type, `Parse::Embeddings` provider registry shipping built-in adapters for OpenAI, Cohere (v3 + v4.0 Matryoshka text-mode), Voyage (incl. open-weight `voyage-4-nano` and `voyage-multimodal-3` text-mode), Jina v3/v4/v5/code, Qwen 3 (DashScope), and a generic `LocalHTTP` client for Ollama / LM Studio / vLLM / TEI. `Klass.find_similar(vector:/text:, k:)` over Atlas `$vectorSearch`, and an `embed` class macro that digest-tracks source fields so vectors only recompute when content changes
10
+ - **`Parse::Cache::Redis`** — Moneta-compatible Redis cache wrapper with a built-in `ConnectionPool`, optional `cache_namespace:` for multi-tenant Redis sharing, and graceful degrade on pool saturation
11
+ - **`ActiveSupport::Notifications` instrumentation** — `parse.cache.*`, `parse.mongodb.aggregate`, `parse.mongodb.find`, and `parse.embeddings.embed` events with stable, PII-safe payload schemas; in-core slow-query log via `Parse.slow_query_threshold_ms`
12
+ - **MCP transport hardening** — Streamable HTTP `Mcp-Session-Id` header (renamed from `X-MCP-Session-Id`, **breaking**), `MCP-Protocol-Version` validation, `DELETE /` session termination, structured-content (`outputSchema`) on built-in tools, optional `health_path:` liveness probe
13
+ - **`Parse::GraphQL::TypeGenerator`** — generate `graphql-ruby` types directly from your `Parse::Object` subclasses (no Parse Server round-trip), with `:vector` columns surfaced as `[Float]` and association registries (`has_one_associations`, `has_many_associations`) populated at DSL time
14
+ - **LiveQuery promoted to stable** — the experimental warning is removed; `Parse.live_query_enabled = true` is retained as a network-egress safety toggle, not a stability gate
15
+ - **Server-version deprecation warning** — one-shot warning when connecting to Parse Server below the supported floor (currently 7.0.0); silence with `Parse.suppress_server_version_warning = true`
16
+ - **`mongo_relation_index :field, dedup: true`** — register a compound `{owningId, relatedId}` UNIQUE on relation join collections to prevent duplicate-pair subscriptions without breaking `has_many` semantics
17
+
18
+ See [CHANGELOG.md](./CHANGELOG.md) for the full 5.0 entry, including security-hardening notes and Ruby 3.x cleanup.
19
+
20
+ ### Core capabilities
10
21
 
11
- **This is an extended and enhanced fork with additional features including:**
12
22
  - MongoDB Aggregation Framework support
13
- - **MongoDB Atlas Search** - Full-text search, autocomplete, faceted search with direct MongoDB access
14
- - **Direct MongoDB Queries** - Bypass Parse Server for high-performance read operations
15
- - **Schema Introspection & Migration** - Compare local models with server schema and generate migrations
16
- - **Enhanced Role Management** - Helper methods for role hierarchies, user management, and membership queries
17
- - **Read Preference Support** - Direct read queries to MongoDB secondary replicas
18
- - **Class-Level Permissions (CLP)** - Define and filter protected fields based on roles and user ownership
19
- - Advanced ACL query constraints (readable_by, writable_by)
20
- - **Owner-aware default ACL policy** (`acl_policy :owner_else_private`) — declare per-class defaults that grant read/write only to the record's owner, with secure or public fallback for server-context creates
23
+ - **MongoDB Atlas Search** full-text search, autocomplete, faceted search with direct MongoDB access
24
+ - **Direct MongoDB Queries** bypass Parse Server's REST surface for high-performance reads, with SDK-side ACL/CLP/`protectedFields` enforcement for scoped agents
25
+ - **Schema Introspection & Migration** compare local models with server schema and generate migrations
26
+ - **Enhanced Role Management** helper methods for role hierarchies, user membership, and subscription queries
27
+ - **Read Preference Support** route reads to MongoDB secondary replicas
28
+ - **Class-Level Permissions (CLP)** define and filter protected fields based on roles and user ownership
29
+ - Advanced ACL query constraints (`readable_by`, `writable_by`)
30
+ - **Owner-aware default ACL policy** (`acl_policy :owner_else_private`) — per-class defaults granting read/write only to the record's owner, with a secure or public fallback for server-context creates
21
31
  - Full transaction support with automatic retry
22
32
  - Comprehensive integration testing with Docker
23
33
  - Enhanced change tracking and webhooks
24
- - Request idempotency system with Retry-After header support
34
+ - Request idempotency with `Retry-After` header support
25
35
  - Timezone support for date operations
26
36
  - Partial fetch with smart autofetch and serialization control
27
37
  - Multi-Factor Authentication (MFA/2FA) support
28
- - LiveQuery real-time subscriptions with TLS/SSL, circuit breaker, health monitoring (experimental)
29
- - AI/LLM Agent integration with security hardening (rate limiting, injection protection)
30
- - And many more improvements (see [CHANGELOG.md](./CHANGELOG.md))
38
+ - LiveQuery real-time subscriptions with TLS/SSL, circuit breaker, and health monitoring
39
+ - AI/LLM agent integration (MCP-spec compliant) with security hardening rate limiting, injection protection, agent ACL scopes
31
40
 
32
- Below is a [quick start guide](#overview). See also the [Usage Guide](./USAGE_GUIDE.md) for practical examples covering queries, aggregation, ACLs, and more.
41
+ Below is a [quick start guide](#overview). See also the [Usage Guide](./docs/usage_guide.md) for practical examples covering queries, aggregation, ACLs, and more.
33
42
 
34
- > **Note:** The [Modernistik API Reference](https://www.modernistik.com/gems/parse-stack/index.html) documents v1.9 only and does not cover features added in v2.x or v3.x.
43
+ > **Note:** API reference docs are published at [neurosynq.github.io/parse-stack-next](https://neurosynq.github.io/parse-stack-next/index.html). Generated via YARD from the current source; covers the full 5.x surface.
35
44
 
36
45
  ### Credits
37
46
 
38
- This project is based on the excellent [Parse Stack framework](https://github.com/modernistik/parse-stack) originally created by [Modernistik](https://www.modernistik.com). We are grateful for their foundational work and continue to build upon it.
47
+ This project (`parse-stack-next`) is a continuation of the [Parse Stack framework](https://github.com/modernistik/parse-stack) originally created by [Modernistik](https://www.modernistik.com). We are grateful for their foundational work and continue to build upon it under the [neurosynq](https://github.com/neurosynq) organization.
39
48
 
40
49
  ### Code Status
41
- [![Gem Version](https://img.shields.io/gem/v/parse-stack.svg)](https://github.com/neurosynq/parse-stack-next)
42
- [![Downloads](https://img.shields.io/gem/dt/parse-stack.svg)](https://rubygems.org/gems/parse-stack)
50
+ [![Gem Version](https://img.shields.io/gem/v/parse-stack-next.svg)](https://rubygems.org/gems/parse-stack-next)
51
+ [![Downloads](https://img.shields.io/gem/dt/parse-stack-next.svg)](https://rubygems.org/gems/parse-stack-next)
43
52
  [![Releases](https://img.shields.io/github/v/release/neurosynq/parse-stack-next)](https://github.com/neurosynq/parse-stack-next/releases)
44
53
 
45
54
  #### Tutorial Videos
55
+
56
+ The following videos were recorded for the original parse-stack gem. The model, query, and association surface they cover is unchanged in parse-stack-next, so they remain a useful introduction; see the [Usage Guide](./docs/usage_guide.md) for v5.x-specific features (vector search, Redis cache, agent tools).
57
+
46
58
  1. Getting Started: https://youtu.be/zoYSGmciDlQ
47
59
  2. Custom Classes and Relations: https://youtu.be/tfSesotfU7w
48
60
  3. Working with Existing Schemas: https://youtu.be/EJGPT7YWyXA
@@ -55,22 +67,21 @@ Add this line to your application's `Gemfile`:
55
67
  ```ruby
56
68
  gem 'parse-stack-next'
57
69
  ```
58
-
59
- > The gem is published as `parse-stack-next` on RubyGems. Older references in this README to `parse-stack` point to the same codebase — use `parse-stack-next` in new projects.
60
-
61
70
  And then execute:
62
71
  ```bash
63
72
  $ bundle
64
73
  ```
65
74
  Or install it yourself as:
66
75
  ```bash
67
- $ gem install parse-stack
76
+ $ gem install parse-stack-next
68
77
  ```
78
+
79
+ > **Note:** The Ruby require path and module namespace are unchanged. You still `require 'parse/stack'` and reference classes under `Parse::Object`, `Parse::Query`, etc. Only the gem name on RubyGems has changed.
69
80
  ### Rack / Sinatra
70
81
  Parse-Stack API, models and webhooks easily integrate in your existing Rack/Sinatra based applications.
71
82
 
72
83
  ### Rails
73
- Parse-Stack comes with support for Rails by adding additional rake tasks and generators. After adding `parse-stack` as a gem dependency in your Gemfile and running `bundle`, you should run the install script:
84
+ Parse-Stack comes with support for Rails by adding additional rake tasks and generators. After adding `parse-stack-next` as a gem dependency in your Gemfile and running `bundle`, you should run the install script:
74
85
  ```bash
75
86
  $ rails g parse_stack:install
76
87
  ```
@@ -162,148 +173,53 @@ result = Parse.call_function :myFunctionName, {param: value}
162
173
 
163
174
  ```
164
175
 
165
- ## What's New in 3.x
166
-
167
- **Current version: 4.4.3** | **Ruby 3.2+ required**
168
-
169
- ### 4.4.3 - Pointer-Shape Strictness & Pipeline Forward-Pass
170
-
171
- - **`Parse.strict_pointer_shapes`** — opt-in flag (or `PARSE_STRICT_POINTER_SHAPES=true`)
172
- that converts unresolvable pointer-shape constraints from a one-shot
173
- warning + silent-zero result into a `Parse::Query::PointerShapeError`
174
- raise. Recommended for test/CI and any LLM-driven workload where
175
- "0 results" reads as a real answer instead of a shape mismatch.
176
- - **Pipeline forward-pass field tracking** — the agent's pipeline
177
- access-policy walker now tracks fields introduced by upstream stages
178
- (`$group._id` and accumulator keys, `$addFields`/`$set` outputs,
179
- `$lookup.as`). The canonical "group → match → sort → limit" pattern
180
- on synthetic accumulator outputs no longer hits `:field_denied`
181
- against the source class's `agent_fields` allowlist.
182
- - **Pointer `query_hint:` in `get_schema`** — every Pointer field
183
- surfaces an inline shape hint covering equality (objectId string OR
184
- full `{__type: "Pointer", ...}` hash) and `$in/$nin` (bare-id array
185
- with SDK-side normalization). Hidden targets collapse to a
186
- `<targetClass>` placeholder.
187
-
188
- ### 4.4.2 - Schema-Aware Walker, Pipeline-Local Aliases
189
-
190
- - **Schema-aware expression-value rewriter** — the `$author` → `$_p_author`
191
- pretty-name rewrite inside expression values now consults the queried
192
- class's declared properties. References whose name is neither a Parse
193
- property nor a universal built-in pass through verbatim, so aliases
194
- introduced by an upstream `$project`/`$addFields`/`$set`/`$group`
195
- stage survive into downstream stages exactly as written. Result rows
196
- are keyed by the literal alias the caller used.
197
- - **`first_or_create!` accepts query-option keys in `synchronize:`** —
198
- filter-lock fingerprint includes constraint operators
199
- (`Parse::Operation` keys) so locks scoped on inequality/range
200
- constraints no longer collide across distinct callers.
201
-
202
- ### 4.4.1 - Filter-Lock Compatibility
203
-
204
- - **`create_lock` accepts `Parse::Operation` keys** — the synchronize
205
- lock-key derivation handles operator-shaped query attributes
206
- (`:status.gt => Date.today`) the same way `Parse::Query` does,
207
- matching the filter-lock fingerprint against the resolved storage
208
- fields.
209
-
210
- ### 4.4 - MongoDB Index Management, CLP on Mongo-Direct, Agent ACL Scope
211
-
212
- - **`Model.describe`** — operator-facing introspection aggregator on every
213
- `Parse::Object` subclass. Reports local model declarations, server
214
- schema, CLP, default ACLs, Atlas Search indexes, and MongoDB indexes.
215
- Local-only by default; opt into server fetches with `network: true`.
216
- See [docs/mongodb_direct_guide.md](docs/mongodb_direct_guide.md).
217
- - **`mongo_index` DSL** — model-declarative MongoDB indexes:
218
- `mongo_index :title, :year`, `mongo_index :vin, unique: true`,
219
- `mongo_geo_index :location`, `mongo_relation_index :users, bidirectional: true`.
220
- Validation (pointer auto-rewrite, parallel-array rejection, `_id` guard,
221
- 64-cap, idempotency) runs at class load.
222
- - **`parse_reference` auto-indexing** — every `parse_reference` field
223
- auto-registers a `unique: true, sparse: true` index declaration.
224
- Removes the operator-must-remember dedup floor. Opt out per-field
225
- with `index: false` or `unique_index: false`.
226
- - **`Parse::MongoDB.configure_writer`** — separate write-capable
227
- connection for index management (`create_index` / `drop_index` /
228
- `writer_indexes`). The reader URI stays read-only. Triple-gated:
229
- writer configured + `index_mutations_enabled = true` +
230
- `ENV["PARSE_MONGO_INDEX_MUTATIONS"]=1` — all re-checked per call.
231
- - **`Parse::Schema::IndexMigrator`** — reconciles declared indexes
232
- against the actual MongoDB state. Plan returns per-collection diff;
233
- apply is additive by default (`drop: true` for orphan removal).
234
- - **`rake parse:mongo:indexes:plan` / `:apply`** — operator workflow
235
- for index migrations. Plan is read-only; apply requires the triple-gate.
236
- - **CLP and `protectedFields` enforced on mongo-direct** — `Parse::CLPScope`
237
- gates `Parse::MongoDB.aggregate` at the operation level for scoped
238
- agents (`session_token:` / `acl_user:` / `acl_role:`) AND strips
239
- `protectedFields` from result rows. The mongo-direct path is the
240
- only first-class enforcement surface for ACL + CLP + protectedFields
241
- on scoped reads (Parse Server's REST aggregate enforces NEITHER).
242
- - **`Parse::Agent.new(acl_user:|acl_role:)`** — declared identity
243
- scope without a session token. Built-in tools auto-promote to
244
- mongo-direct so SDK-side enforcement runs. Sub-agent identity must
245
- be a subset of the parent's reach.
246
- - See [docs/mongodb_index_optimization_guide.md](docs/mongodb_index_optimization_guide.md)
247
- for when to use each index type and how to budget the 64-per-collection cap.
248
-
249
- ### 4.0 - Security Hardening and Modernization
250
- - Minimum Ruby version bumped to 3.2 (Ruby 3.1 reached EOL March 2025)
251
- - Minimum Rails/ActiveSupport bumped to 6.1 (was unbounded at 5.x)
252
- - CI tests against Ruby 3.2, 3.3, 3.4, and 3.5
253
- - LiveQuery TLS hostname verification (`post_connection_check`)
254
- - Webhook endpoint fails closed when no key is configured (opt out via `Parse::Webhooks.allow_unauthenticated = true`)
255
- - `Parse::Error.new(code, message)` two-argument constructor with `#code` reader
256
- - `include`d pointer fields now auto-added to `keys` when a key allowlist is set
257
-
258
- ### 3.3 - Ruby Version Update
259
- - Minimum Ruby version bumped to 3.1 (Ruby 3.0 reached EOL March 2024)
260
- - CI tests against Ruby 3.1, 3.2, 3.3, and 3.4
261
-
262
- ### 3.2 - Class-Level Permissions (CLP)
263
- Define operation permissions and protected fields directly in models:
176
+ ## Release History
264
177
 
265
- ```ruby
266
- class Document < Parse::Object
267
- set_clp :find, public: true
268
- set_clp :delete, public: false, roles: ["Admin"]
269
- protect_fields "*", [:internal_notes, :secret_data]
270
- end
271
- ```
178
+ **Current version: 5.0.1** | **Ruby 3.2+ required**
179
+
180
+ The 5.0 highlights (vector search / RAG, pooled Redis cache, AS::N instrumentation, MCP transport hardening, GraphQL type generation) are summarized in the [What's new in 5.0](#whats-new-in-50) section above. Earlier releases are recorded below.
181
+
182
+ Per-version detail lives in [CHANGELOG.md](./CHANGELOG.md) and on the [Releases page](https://github.com/neurosynq/parse-stack-next/releases). The compact summary below is the major-line view.
272
183
 
273
- ### 3.1 - Atlas Search, MongoDB Direct, Schema Tools
274
- - **MongoDB Atlas Search** - Full-text search, autocomplete, faceted search
275
- - **Direct MongoDB Queries** - `results_direct`, `first_direct` bypassing Parse Server
276
- - **Schema Introspection** - `Parse::Schema.diff`, `Parse::Schema.migration`
277
- - **Read Preference** - `read_pref(:secondary)` for replica set reads
278
- - **Role Management** - `find_or_create`, `add_users`, `add_child_role`, `all_users`
184
+ ### 4.x MongoDB index management, agent ACL scope, CLP enforced on mongo-direct, and `parse-stack-next` debut
279
185
 
280
- ### 3.0 - Push, Sessions, AI Agent
281
- - **Push Builder API** - Fluent pattern with `to_channel`, `with_alert`, `silent!`, `send!`
282
- - **Session Management** - `expired?`, `time_remaining`, `logout_all!`
283
- - **Installation Channels** - `subscribe`, `unsubscribe`, `subscribed_to?`
284
- - **AI/LLM Agent** - `Parse::Agent` with natural language queries
285
- - **MFA Support** - TOTP and SMS-based two-factor authentication
286
- - **LiveQuery** (Experimental) - Real-time WebSocket subscriptions
186
+ - **`mongo_index` DSL** (`mongo_index`, `mongo_geo_index`, `mongo_relation_index`) with class-load validation (pointer auto-rewrite, parallel-array rejection, `_id` guard, 64-per-collection cap). `parse_reference` fields auto-register a unique-sparse index.
187
+ - **Index migration tooling** — `Parse::MongoDB.configure_writer` (separate write connection, triple-gated), `Parse::Schema::IndexMigrator` (plan / apply with optional orphan drop), and `rake parse:mongo:indexes:plan` / `:apply`.
188
+ - **`Model.describe`** operator introspection aggregator (local declarations + optional server fetch covering schema, CLP, default ACLs, Atlas Search, MongoDB indexes).
189
+ - **CLP + `protectedFields` enforced on mongo-direct** `Parse::CLPScope` gates `Parse::MongoDB.aggregate` for scoped agents (`session_token:` / `acl_user:` / `acl_role:`) and strips protected fields from result rows. This is the only first-class enforcement surface for ACL + CLP + protectedFields on scoped reads; Parse Server's REST aggregate enforces neither.
190
+ - **`Parse::Agent.new(acl_user:|acl_role:)`** — declared agent identity without a session token; built-in tools auto-promote to mongo-direct. Sub-agent identity must be a subset of the parent's reach.
191
+ - **Pipeline correctness** — schema-aware `$author` `$_p_author` rewriter respects pipeline-local aliases; forward-pass field tracking through `$group`/`$addFields`/`$set`/`$lookup.as`; pointer `query_hint:` surfaced in `get_schema`.
192
+ - **`Parse.strict_pointer_shapes`** opt-in flag that converts unresolvable pointer-shape constraints into a `PointerShapeError` raise (recommended for test/CI and LLM-driven workloads).
193
+ - **`first_or_create!` / `create_lock` accept `Parse::Operation` keys** in `synchronize:`, fixing filter-lock fingerprint collisions on inequality/range constraints.
194
+ - **Security & modernization** — Ruby 3.2 floor, Rails/ActiveSupport 6.1 floor, CI on Ruby 3.2–3.5. LiveQuery TLS hostname verification. Webhook endpoint fails closed when no key is configured. `Parse::Error.new(code, message)` two-argument constructor. `include`d pointer fields auto-added to `keys` when an allowlist is set.
195
+ - **4.5.0 — first release of this gem.** `parse-stack-next` debuts on RubyGems under the [neurosynq](https://github.com/neurosynq) organization, continuing from the upstream `parse-stack` 4.4.x line. The Ruby require path (`require 'parse/stack'`) and the `Parse::*` namespace are unchanged from upstream — only the gem name on RubyGems is new.
287
196
 
288
- ## What's New in 2.x
197
+ ### 3.x Atlas Search, MongoDB-direct, CLP, AI agent, push, MFA, LiveQuery
289
198
 
290
- **Ruby 3.0+ required** | See detailed docs in later sections
199
+ - **MongoDB Atlas Search** full-text search, autocomplete, faceted search.
200
+ - **Direct MongoDB queries** — `results_direct`, `first_direct`, `count_direct` bypassing Parse Server's REST surface.
201
+ - **Schema tools** — `Parse::Schema.diff`, `Parse::Schema.migration`, plus `read_pref(:secondary)` for replica reads.
202
+ - **Role management** — `find_or_create`, `add_users`, `add_child_role`, `all_users` (with hierarchy walks).
203
+ - **Class-Level Permissions (CLP)** declared in models — `set_clp :find, public: true`, `protect_fields "*", [:internal_notes]`.
204
+ - **AI/LLM agent** — `Parse::Agent` with natural-language queries over a tool interface.
205
+ - **Push builder API** — `to_channel`, `with_alert`, `silent!`, `send!`; installation channels (`subscribe`, `unsubscribe`).
206
+ - **Session lifecycle** — `expired?`, `time_remaining`, `logout_all!`.
207
+ - **MFA** — TOTP and SMS two-factor authentication.
208
+ - **LiveQuery** — real-time WebSocket subscriptions (promoted to stable in 5.0).
209
+ - **Ruby 3.1 floor** (3.0 reached EOL March 2024).
291
210
 
292
- ### Key Features
293
- - **Transactions** - `Parse::Object.transaction` with automatic retry
294
- - **MongoDB Aggregation** - `group_by`, `count_distinct`, custom pipelines
295
- - **ACL Query Constraints** - `readable_by`, `writable_by`, `publicly_readable`
296
- - **Request Idempotency** - Automatic duplicate prevention (enabled by default)
297
- - **Enhanced Change Tracking** - Works correctly in `after_save` hooks
298
- - **LiveQuery** (Experimental) - Real-time subscriptions with circuit breaker
211
+ ### 2.x — aggregation, transactions, idempotency, ACL constraints
299
212
 
300
- ### Breaking Changes from 1.x
301
- - Minimum Ruby 3.0+
302
- - `distinct` returns object IDs by default (use `return_pointers: true` for pointers)
303
- - Faraday 2.x (removed faraday_middleware)
304
- - Fixed typo "constaint" to "constraint"
213
+ - **Transactions** `Parse::Object.transaction` with automatic retry.
214
+ - **MongoDB aggregation** — `group_by`, `count_distinct`, custom pipelines.
215
+ - **ACL query constraints** `readable_by`, `writable_by`, `publicly_readable`.
216
+ - **Request idempotency** automatic duplicate prevention, enabled by default.
217
+ - **Change tracking** works correctly in `after_save` hooks.
218
+ - **Breaking from 1.x** — Ruby 3.0 floor, Faraday 2.x (no `faraday_middleware`), `distinct` returns object IDs by default (pass `return_pointers: true` for pointers), `constaint` → `constraint` typo fix.
305
219
 
306
- For complete details, see the [CHANGELOG](./CHANGELOG.md) and [Releases](https://github.com/neurosynq/parse-stack-next/releases).
220
+ ### 1.x initial Parse Server SDK
221
+
222
+ The 1.x line is the original [`modernistik/parse-stack`](https://github.com/modernistik/parse-stack) — Active Model ORM, REST client, query DSL, associations, and Cloud Code webhooks for Parse Server. `parse-stack-next` is a continuation of that work; the first release published under the new gem name is **4.5.0** (above), on RubyGems as [`parse-stack-next`](https://rubygems.org/gems/parse-stack-next).
307
223
 
308
224
  ## Table of Contents
309
225
 
@@ -448,6 +364,7 @@ For complete details, see the [CHANGELOG](./CHANGELOG.md) and [Releases](https:/
448
364
  - [Saved Audiences](#saved-audiences)
449
365
  - [Push Status Tracking](#push-status-tracking)
450
366
  - [Installation Channel Management](#installation-channel-management)
367
+ - [Analytics](#analytics)
451
368
  - [Cloud Code Webhooks](#cloud-code-webhooks)
452
369
  - [Cloud Code Functions](#cloud-code-functions)
453
370
  - [Cloud Code Triggers](#cloud-code-triggers)
@@ -476,16 +393,16 @@ For complete details, see the [CHANGELOG](./CHANGELOG.md) and [Releases](https:/
476
393
  ## Architecture
477
394
  The architecture of `Parse::Stack` is broken into four main components.
478
395
 
479
- ### [Parse::Client](https://www.modernistik.com/gems/parse-stack/Parse/Client.html)
396
+ ### [Parse::Client](https://neurosynq.github.io/parse-stack-next/Parse/Client.html)
480
397
  This class is the core and low level API for the Parse Server REST interface that is used by the other components. It can manage multiple sessions, which means you can have multiple client instances pointing to different Parse Server applications at the same time. It handles sending raw requests as well as providing Request/Response objects for all API handlers. The connection engine is Faraday, which means it is open to add any additional middleware for features you'd like to implement.
481
398
 
482
- ### [Parse::Query](https://www.modernistik.com/gems/parse-stack/Parse/Query.html)
399
+ ### [Parse::Query](https://neurosynq.github.io/parse-stack-next/Parse/Query.html)
483
400
  This class implements the [Parse REST Querying](http://docs.parseplatform.org/rest/guide/#queries) interface in the [DataMapper finder syntax style](http://datamapper.org/docs/find.html). It compiles a set of query constraints and utilizes `Parse::Client` to send the request and provide the raw results. This class can be used without the need to define models.
484
401
 
485
- ### [Parse::Object](https://www.modernistik.com/gems/parse-stack/Parse/Object.html)
402
+ ### [Parse::Object](https://neurosynq.github.io/parse-stack-next/Parse/Object.html)
486
403
  This component is main class for all object relational mapping subclasses for your application. It provides features in order to map your remote Parse records to a local ruby object. It implements the Active::Model interface to provide a lot of additional features, CRUD operations, querying, including dirty tracking, JSON serialization, save/destroy callbacks and others. While we are overlooking some functionality, for simplicity, you will mainly be working with Parse::Object as your superclass. While not required, it is highly recommended that you define a model (Parse::Object subclass) for all the Parse classes in your application.
487
404
 
488
- ### [Parse::Webhooks](https://www.modernistik.com/gems/parse-stack/Parse/Webhooks.html)
405
+ ### [Parse::Webhooks](https://neurosynq.github.io/parse-stack-next/Parse/Webhooks.html)
489
406
  Parse provides a feature called [Cloud Code Webhooks](http://blog.parse.com/announcements/introducing-cloud-code-webhooks/). For most applications, save/delete triggers and cloud functions tend to be implemented by Parse's own hosted Javascript solution called Cloud Code. However, Parse provides the ability to have these hooks utilize your hosted solution instead of their own, since their environment is limited in terms of resources and tools.
490
407
 
491
408
  ## Field Naming Conventions
@@ -647,6 +564,10 @@ As a shortcut, if you are planning on using REDIS and have configured the use of
647
564
  Parse.setup(cache: 'redis://localhost:6379', ...)
648
565
  ```
649
566
 
567
+ Redis is the recommended cache backend for multi-process / multi-dyno deployments: an in-memory `Moneta.new(:Memory)` store is local to a single Ruby process, so two Puma workers (or two web dynos) each hold their own cache and a write through one will not invalidate the other. A shared Redis backend gives every process the same view, and the existing PUT/POST/DELETE invalidation in `Parse::Middleware::Caching` runs against the shared store. Cache reads degrade gracefully on `Redis::CannotConnectError` / `Redis::TimeoutError` — the middleware disables caching for the failing request and lets the underlying GET pass through to Parse Server.
568
+
569
+ The cache surface is opt-in at two layers. Object fetches (`Model.find(id)`, `obj.reload!` in non-write-only mode) cache by default once a store is configured. Query results do **not** cache by default — pass `cache: true` per call (e.g. `Song.all(limit: 500, cache: true)`) or set `Parse.default_query_cache = true` for opt-out behavior. Both layers honor `cache: false` / `Cache-Control: no-cache` to skip the cache for an individual request.
570
+
650
571
  #### `:expires`
651
572
  Sets the default cache expiration time (in seconds) for successful non-empty `GET` requests when using the caching middleware. The default value is 3 seconds. If `:expires` is set to 0, caching will be disabled. You can always clear the current state of the cache using the `clear_cache!` method on your `Parse::Client` instance.
652
573
 
@@ -771,7 +692,7 @@ You can always combine both approaches by defining special attributes before you
771
692
 
772
693
  ```
773
694
 
774
- ## [Parse Config](https://www.modernistik.com/gems/parse-stack/Parse/API/Config.html)
695
+ ## [Parse Config](https://neurosynq.github.io/parse-stack-next/Parse/API/Config.html)
775
696
  Getting your configuration variables once you have a default client setup can be done with `Parse.config`. The first time this method is called, Parse-Stack will get the configuration from Parse Server, and cache it. To force a reload of the config, use `config!`. You
776
697
 
777
698
  ```ruby
@@ -794,7 +715,7 @@ Getting your configuration variables once you have a default client setup can be
794
715
  ## Core Classes
795
716
  While some native data types are similar to the ones supported by Ruby natively, other ones are more complex and require their dedicated classes.
796
717
 
797
- ### [Parse::Pointer](https://www.modernistik.com/gems/parse-stack/Parse/Pointer.html)
718
+ ### [Parse::Pointer](https://neurosynq.github.io/parse-stack-next/Parse/Pointer.html)
798
719
  An important concept is the `Parse::Pointer` class. This is the superclass of `Parse::Object` and represents the pointer type in Parse. A `Parse::Pointer` only contains data about the specific Parse class and the `id` for the object. Therefore, creating an instance of any Parse::Object subclass with only the `:id` field set will be considered in "pointer" state even though its specific class is not `Parse::Pointer` type. The only case that you may have a Parse::Pointer is in the case where an object was received for one of your classes and the framework has no registered class handler for it. Using the example above, assume you have the tables `Post`, `Comment` and `Author` defined in your remote Parse application, but have only defined `Post` and `Commentary` locally.
799
720
 
800
721
  ```ruby
@@ -858,7 +779,7 @@ pointer.title # Raises Parse::AutofetchTriggeredError instead of fetching
858
779
 
859
780
  The effect is that for any unknown classes that the framework encounters, it will generate Parse::Pointer instances until you define those classes with valid properties and associations. While this might be ok for some classes you do not use, we still recommend defining all your Parse classes locally in the framework.
860
781
 
861
- ### [Parse::File](https://www.modernistik.com/gems/parse-stack/Parse/File.html)
782
+ ### [Parse::File](https://neurosynq.github.io/parse-stack-next/Parse/File.html)
862
783
  This class represents a Parse file pointer. `Parse::File` has helper methods to upload Parse files directly to Parse and manage file associations with your classes. Using our Song class example:
863
784
 
864
785
  ```ruby
@@ -912,7 +833,7 @@ file.url # => https://www.example.com/file.png
912
833
 
913
834
  ```
914
835
 
915
- ### [Parse::Date](https://www.modernistik.com/gems/parse-stack/Parse/Date.html)
836
+ ### [Parse::Date](https://neurosynq.github.io/parse-stack-next/Parse/Date.html)
916
837
  This class manages dates in the special JSON format it requires for properties of type `:date`. `Parse::Date` subclasses `DateTime`, which allows you to use any features or methods available to `DateTime` with `Parse::Date`. While the conversion between `Time` and `DateTime` objects to a `Parse::Date` object is done implicitly for you, you can use the added special methods, `DateTime#parse_date` and `Time#parse_date`, for special occasions.
917
838
 
918
839
  ```ruby
@@ -921,7 +842,7 @@ This class manages dates in the special JSON format it requires for properties o
921
842
  song.save # ok
922
843
  ```
923
844
 
924
- ### [Parse::GeoPoint](https://www.modernistik.com/gems/parse-stack/Parse/GeoPoint.html)
845
+ ### [Parse::GeoPoint](https://neurosynq.github.io/parse-stack-next/Parse/GeoPoint.html)
925
846
  This class manages the GeoPoint data type that Parse provides to support geo-queries. To define a GeoPoint property, use the `:geopoint` data type. Please note that latitudes should not be between -90.0 and 90.0, and longitudes should be between -180.0 and 180.0.
926
847
 
927
848
  ```ruby
@@ -953,7 +874,7 @@ We include helper methods to calculate distances between GeoPoints: `distance_in
953
874
  # ~180.793 km
954
875
  ```
955
876
 
956
- ### [Parse::Bytes](https://www.modernistik.com/gems/parse-stack/Parse/Bytes.html)
877
+ ### [Parse::Bytes](https://neurosynq.github.io/parse-stack-next/Parse/Bytes.html)
957
878
  The `Bytes` data type represents the storage format for binary content in a Parse column. The content is needs to be encoded into a base64 string.
958
879
 
959
880
  ```ruby
@@ -965,7 +886,7 @@ The `Bytes` data type represents the storage format for binary content in a Pars
965
886
  decoded = bytes.decoded # same as Base64.decode64
966
887
  ```
967
888
 
968
- ### [Parse::TimeZone](https://www.modernistik.com/gems/parse-stack/Parse/TimeZone.html)
889
+ ### [Parse::TimeZone](https://neurosynq.github.io/parse-stack-next/Parse/TimeZone.html)
969
890
  While Parse does not provide a native time zone data type, Parse-Stack provides a class to make it easier to manage time zone attributes, usually stored IANA string identifiers, with your ruby code. This is done by utilizing the features provided by [`ActiveSupport::TimeZone`](http://api.rubyonrails.org/classes/ActiveSupport/TimeZone.html). In addition to setting a column as a time zone field, we also add special validations to verify it is of the right IANA identifier.
970
891
 
971
892
  ```ruby
@@ -988,9 +909,9 @@ event.time_zone = 'Galaxy/Andromeda'
988
909
  event.time_zone.valid? # => false
989
910
  ```
990
911
 
991
- ### [Parse::ACL](https://www.modernistik.com/gems/parse-stack/Parse/ACL.html)
912
+ ### [Parse::ACL](https://neurosynq.github.io/parse-stack-next/Parse/ACL.html)
992
913
  The `ACL` class represents the access control lists for each record. An ACL is represented by a JSON object with the keys being `Parse::User` object ids or the special key of `*`, which indicates the public access permissions.
993
- The value of each key in the hash is a [`Parse::ACL::Permission`](https://www.modernistik.com/gems/parse-stack/Parse/ACL/Permission.html) object which defines the boolean permission state for `read` and `write`.
914
+ The value of each key in the hash is a [`Parse::ACL::Permission`](https://neurosynq.github.io/parse-stack-next/Parse/ACL/Permission.html) object which defines the boolean permission state for `read` and `write`.
994
915
 
995
916
  The example below illustrates a Parse ACL JSON object where there is a public read permission, but public write is prevented. In addition, the user with id `3KmCvT7Zsb` and the `Admins` role, are allowed to both read and write on this record.
996
917
 
@@ -1056,9 +977,15 @@ There are four policies:
1056
977
  | Policy | When an owner is resolvable | When no owner is resolvable |
1057
978
  |---|---|---|
1058
979
  | `:public` | public read + write | public read + write |
980
+ | `:public_read` | public read, master-key write | public read, master-key write |
1059
981
  | `:private` | master-key only | master-key only |
1060
982
  | `:owner_else_public` | owner read + write only | public read + write |
1061
983
  | `:owner_else_private` | owner read + write only | master-key only |
984
+ | `:owner_but_public_read` | owner read + write *and* public read | public read, master-key write |
985
+
986
+ `:public_read` (v5.0+) stamps `{"*": {"read": true}}` — anyone can read the row, but no client can mutate it through ACL (only the master key can write). Useful for catalog / lookup / reference data.
987
+
988
+ `:owner_but_public_read` (v5.0+) is the "single-author public post" case: the resolved owner gets full R/W and the rest of the world gets read-only access in the same ACL — `{"*": {"read": true}, "<ownerId>": {"read": true, "write": true}}`. When no owner resolves at save (no `as:` and no resolvable `owner:` field), it degrades to `:public_read` semantics rather than the all-or-nothing fallback used by the `:owner_else_*` family.
1062
989
 
1063
990
  ```ruby
1064
991
  class Post < Parse::Object
@@ -1320,8 +1247,8 @@ clp.filter_fields(doc_data, user: "other_user")
1320
1247
 
1321
1248
  This also works with arrays of pointers (e.g., `owners: [user1, user2]`).
1322
1249
 
1323
- ### [Parse::Session](https://www.modernistik.com/gems/parse-stack/Parse/Session.html)
1324
- This class represents the data and columns contained in the standard Parse `_Session` collection. You may add additional properties and methods to this class. See [Session API Reference](https://www.modernistik.com/gems/parse-stack/Parse/Session.html). You may call `Parse.use_shortnames!` to use `Session` in addition to `Parse::Session`.
1250
+ ### [Parse::Session](https://neurosynq.github.io/parse-stack-next/Parse/Session.html)
1251
+ This class represents the data and columns contained in the standard Parse `_Session` collection. You may add additional properties and methods to this class. See [Session API Reference](https://neurosynq.github.io/parse-stack-next/Parse/Session.html). You may call `Parse.use_shortnames!` to use `Session` in addition to `Parse::Session`.
1325
1252
 
1326
1253
  You can get a specific `Parse::Session` given a session_token by using the `session` method. You can also find the user tied to a specific Parse session or session token with `Parse::User.session`.
1327
1254
 
@@ -1341,19 +1268,19 @@ some_object.destroy( session: user.session_token )
1341
1268
 
1342
1269
  ```
1343
1270
 
1344
- ### [Parse::Installation](https://www.modernistik.com/gems/parse-stack/Parse/Installation.html)
1345
- This class represents the data and columns contained in the standard Parse `_Installation` collection. You may add additional properties and methods to this class. See [Installation API Reference](https://www.modernistik.com/gems/parse-stack/Parse/Installation.html). You may call `Parse.use_shortnames!` to use `Installation` in addition to `Parse::Installation`.
1271
+ ### [Parse::Installation](https://neurosynq.github.io/parse-stack-next/Parse/Installation.html)
1272
+ This class represents the data and columns contained in the standard Parse `_Installation` collection. You may add additional properties and methods to this class. See [Installation API Reference](https://neurosynq.github.io/parse-stack-next/Parse/Installation.html). You may call `Parse.use_shortnames!` to use `Installation` in addition to `Parse::Installation`.
1346
1273
 
1347
- ### [Parse::Product](https://www.modernistik.com/gems/parse-stack/Parse/Product.html)
1348
- This class represents the data and columns contained in the standard Parse `_Product` collection. You may add additional properties and methods to this class. See [Product API Reference](https://www.modernistik.com/gems/parse-stack/Parse/Product.html). You may call `Parse.use_shortnames!` to use `Product` in addition to `Parse::Product`.
1274
+ ### [Parse::Product](https://neurosynq.github.io/parse-stack-next/Parse/Product.html)
1275
+ This class represents the data and columns contained in the standard Parse `_Product` collection. You may add additional properties and methods to this class. See [Product API Reference](https://neurosynq.github.io/parse-stack-next/Parse/Product.html). You may call `Parse.use_shortnames!` to use `Product` in addition to `Parse::Product`.
1349
1276
 
1350
1277
  The `_Product` collection backs the original Parse iOS SDK's `PFProduct` downloadable-content in-app-purchase flow. That feature was tied to hosted Parse and is not actively used by modern Parse Server deployments — most apps now verify in-app purchase receipts directly against the Apple App Store or Google Play. The class is retained for backwards compatibility with legacy applications that still read or write product metadata. It is also marked `agent_hidden` by default so it does not surface through MCP / agent tooling; applications that genuinely need agent access can call `Parse::Product.agent_unhidden` at boot.
1351
1278
 
1352
- ### [Parse::Role](https://www.modernistik.com/gems/parse-stack/Parse/Role.html)
1353
- This class represents the data and columns contained in the standard Parse `_Role` collection. You may add additional properties and methods to this class. See [Roles API Reference](https://www.modernistik.com/gems/parse-stack/Parse/Role.html). You may call `Parse.use_shortnames!` to use `Role` in addition to `Parse::Role`.
1279
+ ### [Parse::Role](https://neurosynq.github.io/parse-stack-next/Parse/Role.html)
1280
+ This class represents the data and columns contained in the standard Parse `_Role` collection. You may add additional properties and methods to this class. See [Roles API Reference](https://neurosynq.github.io/parse-stack-next/Parse/Role.html). You may call `Parse.use_shortnames!` to use `Role` in addition to `Parse::Role`.
1354
1281
 
1355
1282
  #### Default ACL (master-only)
1356
- Parse Server requires every `_Role` row to ship with an ACL — the requirement is hard-coded in `SchemaController.requiredColumns` and cannot be disabled by config. `Parse::Role` declares `acl_policy :private`, so every role saved without an explicit ACL is stamped with `{}` (master-key only). This is intentional: anonymous and authenticated-but-non-master clients cannot enumerate role names, read membership, or walk the role hierarchy. Parse Server's internal role-membership expansion (used during ACL evaluation) runs with master context, so the master-only default does not break permission checks on other classes.
1283
+ Parse Server requires every `_Role` row to ship with an ACL — the requirement is hard-coded in `SchemaController.requiredColumns` and cannot be disabled by config. `Parse::Role` declares `acl_policy :private`, so every role saved without an explicit ACL is stamped with `{}` (master-key only). This is intentional: anonymous and authenticated-but-non-master clients cannot enumerate role names, read subscription, or walk the role hierarchy. Parse Server's internal role-subscription expansion (used during ACL evaluation) runs with master context, so the master-only default does not break permission checks on other classes.
1357
1284
 
1358
1285
  To opt into broader access, pass an explicit ACL:
1359
1286
 
@@ -1397,7 +1324,7 @@ admin.child_roles_count # Direct child roles
1397
1324
  admin.total_users_count # All users including child roles
1398
1325
  ```
1399
1326
 
1400
- ### [Parse::JobStatus](https://www.modernistik.com/gems/parse-stack/Parse/JobStatus.html)
1327
+ ### [Parse::JobStatus](https://neurosynq.github.io/parse-stack-next/Parse/JobStatus.html)
1401
1328
 
1402
1329
  This class represents the data and columns contained in the standard Parse `_JobStatus` collection. Parse Server writes a row here every time a background job — registered server-side via `Parse.Cloud.job(...)` — runs, recording its outcome and any status/message updates emitted via `request.message(...)`.
1403
1330
 
@@ -1444,7 +1371,7 @@ Parse::JobStatus.cleanup_older_than!(days: 7, terminal_only: false)
1444
1371
 
1445
1372
  The helper requires master-key access (Parse Server's default `_JobStatus` CLP). Run from a periodic cron or scheduled job to keep `_JobStatus` from growing unboundedly.
1446
1373
 
1447
- ### [Parse::JobSchedule](https://www.modernistik.com/gems/parse-stack/Parse/JobSchedule.html)
1374
+ ### [Parse::JobSchedule](https://neurosynq.github.io/parse-stack-next/Parse/JobSchedule.html)
1448
1375
 
1449
1376
  This class represents the data and columns contained in the standard Parse `_JobSchedule` collection. Rows here define recurring runs for background jobs registered via `Parse.Cloud.job(...)`. The collection is populated by the Parse Dashboard's "Schedule a Job" UI.
1450
1377
 
@@ -1461,8 +1388,8 @@ schedule.parsed_params # => { "dryRun" => false } — JSON-decoded
1461
1388
 
1462
1389
  `params` is stored on the wire as a JSON-encoded **string** per Parse Server's canonical schema (Object columns reject `$` and `.` in nested keys, which would otherwise break common payload shapes). Use `#parsed_params` to decode; it returns `nil` for blank or invalid JSON instead of raising. `last_run` is a raw `Number` whose unit is scheduler-defined — most external schedulers write `Date.now()` milliseconds, but the canonical schema does not pin a unit.
1463
1390
 
1464
- ### [Parse::User](https://www.modernistik.com/gems/parse-stack/Parse/User.html)
1465
- This class represents the data and columns contained in the standard Parse `_User` collection. You may add additional properties and methods to this class. See [User API Reference](https://www.modernistik.com/gems/parse-stack/Parse/User.html). You may call `Parse.use_shortnames!` to use `User` in addition to `Parse::User`.
1391
+ ### [Parse::User](https://neurosynq.github.io/parse-stack-next/Parse/User.html)
1392
+ This class represents the data and columns contained in the standard Parse `_User` collection. You may add additional properties and methods to this class. See [User API Reference](https://neurosynq.github.io/parse-stack-next/Parse/User.html). You may call `Parse.use_shortnames!` to use `User` in addition to `Parse::User`.
1466
1393
 
1467
1394
  #### Signup
1468
1395
  You can signup new users in two ways. You can either use a class method `Parse::User.signup` to create a new user with the minimum fields of username, password and email, or create a `Parse::User` object can call the `signup!` method. If signup fails, it will raise the corresponding exception.
@@ -1931,7 +1858,7 @@ band.drummer # Artist object
1931
1858
  ###### `:field`
1932
1859
  This option allows you to set the name of the remote Parse column for this property. Using this will explicitly set the remote property name to the value of this option. The value provided for this option will affect the name of the alias method that is generated when `alias` option is used. **By default, the name of the remote column is the lower-first camel case version of the property name. As an example, for a property with key `:my_property_name`, the framework will implicitly assume that the remote column is `myPropertyName`.**
1933
1860
 
1934
- #### [Has One](https://www.modernistik.com/gems/parse-stack/Parse/Associations/HasOne.html)
1861
+ #### [Has One](https://neurosynq.github.io/parse-stack-next/Parse/Associations/HasOne.html)
1935
1862
  The `has_one` creates a one-to-one association with another Parse class. This association says that the other class in the association contains a foreign pointer column which references instances of this class. If your model contains a column that is a Parse pointer to another class, you should use `belongs_to` for that association instead.
1936
1863
 
1937
1864
  Defining a `has_one` property generates a helper query method to fetch a particular record from a foreign class. This is useful for setting up the inverse relationship accessors of a `belongs_to`. In the case of the `has_one` relationship, the `:field` option represents the name of the column of the foreign class where the Parse pointer is stored. By default, the lower-first camel case version of the Parse class name is used.
@@ -1990,7 +1917,7 @@ user.band_by_status(false)
1990
1917
 
1991
1918
  ```
1992
1919
 
1993
- #### [Has Many](https://www.modernistik.com/gems/parse-stack/Parse/Associations/HasMany.html)
1920
+ #### [Has Many](https://neurosynq.github.io/parse-stack-next/Parse/Associations/HasMany.html)
1994
1921
  Parse has many ways to implement one-to-many and many-to-many associations: `Array`, `Parse Relation` or through a `Query`. How you decide to implement your associations, will affect how `has_many` works in Parse-Stack. Parse natively supports one-to-many and many-to-many relationships using `Array` and `Relations`, as described in [Relational Data](http://docs.parseplatform.org/js/guide/#relational-data). Both of these methods require you define a specific column type in your Parse table that will be used to store information about the association.
1995
1922
 
1996
1923
  In addition to `Array` and `Relation`, Parse-Stack also implements the standard `has_many` behavior prevalent in other frameworks through a query where the associated class contains a foreign pointer to the local class, usually the inverse of a `belongs_to`. This requires that the associated class has a defined column
@@ -2234,7 +2161,7 @@ author.posts # => Posts where author's name is a tag
2234
2161
  ```
2235
2162
 
2236
2163
  ## Creating, Saving and Deleting Records
2237
- This section provides some of the basic methods when creating, updating and deleting objects from Parse. Additional documentation for these APIs can be found under [Parse::Core::Actions](https://www.modernistik.com/gems/parse-stack/Parse/Core/Actions.html). To illustrate the various methods available for saving Parse records, we use this example class:
2164
+ This section provides some of the basic methods when creating, updating and deleting objects from Parse. Additional documentation for these APIs can be found under [Parse::Core::Actions](https://neurosynq.github.io/parse-stack-next/Parse/Core/Actions.html). To illustrate the various methods available for saving Parse records, we use this example class:
2238
2165
 
2239
2166
  ```ruby
2240
2167
 
@@ -2397,7 +2324,7 @@ To commit a new record or changes to an existing record to Parse, use the `#save
2397
2324
  The save operation can handle both creating and updating existing objects. If you do not want to update the association data of a changed object, you may use the `#update` method to only save the changed property values. In the case where you want to force update an object even though it has not changed, to possibly trigger your `before_save` hooks, you can use the `#update!` method. In addition, just like with other ActiveModel objects, you may call `reload!` to fetch the current record again from the data store.
2398
2325
 
2399
2326
  ### Saving applying User ACLs
2400
- You may save and delete objects from Parse on behalf of a logged in user by passing the session token to the call to `save` or `destroy`. Doing so will allow Parse to apply the ACLs of this user against the record to see if the user is authorized to read or write the record. See [Parse::Actions](https://www.modernistik.com/gems/parse-stack/Parse/Core/Actions.html).
2327
+ You may save and delete objects from Parse on behalf of a logged in user by passing the session token to the call to `save` or `destroy`. Doing so will allow Parse to apply the ACLs of this user against the record to see if the user is authorized to read or write the record. See [Parse::Actions](https://neurosynq.github.io/parse-stack-next/Parse/Core/Actions.html).
2401
2328
 
2402
2329
  ```ruby
2403
2330
  user = Parse::User.login('myuser','pass')
@@ -2902,7 +2829,7 @@ user = User.first(keys: [:id, :first_name, :email])
2902
2829
  user.to_json # Only includes id, first_name, email (plus metadata)
2903
2830
 
2904
2831
  # Useful for webhook responses - returns only requested fields
2905
- Parse::Webhooks.route :function, :getTeamMembers do
2832
+ Parse::Webhooks.route :function, :getWorkspaceMembers do
2906
2833
  users = User.all(:id.in => user_ids, keys: [:id, :first_name, :icon_image])
2907
2834
  users # Returns only the requested fields, no autofetch triggered
2908
2835
  end
@@ -3158,7 +3085,7 @@ users_by_city = User.group_objects_by(:city)
3158
3085
 
3159
3086
  # Advanced options
3160
3087
  User.group_by(:tags, flatten_arrays: true).count # Flatten array fields
3161
- User.group_by(:team, return_pointers: true).count # Use pointers for efficiency
3088
+ User.group_by(:workspace, return_pointers: true).count # Use pointers for efficiency
3162
3089
  ```
3163
3090
 
3164
3091
  **Available aggregation methods:** `count`, `sum(field)`, `min(field)`, `max(field)`, `avg(field)`
@@ -3168,7 +3095,7 @@ User.group_by(:team, return_pointers: true).count # Use pointers for efficiency
3168
3095
  Finds the distinct values for a specified field across a single collection or
3169
3096
  view and returns the results in an array. You may mix this with additional query constraints.
3170
3097
 
3171
- **⚠️ Breaking Change in v1.12.0**: For pointer fields, `distinct` now returns object IDs directly by default instead of full pointer hash objects like `{"__type"=>"Pointer", "className"=>"Team", "objectId"=>"abc123"}`. Use `return_pointers: true` to get Parse::Pointer objects.
3098
+ **⚠️ Breaking Change in v1.12.0**: For pointer fields, `distinct` now returns object IDs directly by default instead of full pointer hash objects like `{"__type"=>"Pointer", "className"=>"Workspace", "objectId"=>"abc123"}`. Use `return_pointers: true` to get Parse::Pointer objects.
3172
3099
 
3173
3100
  ```ruby
3174
3101
  # Return a list of unique city names
@@ -3177,15 +3104,15 @@ view and returns the results in an array. You may mix this with additional query
3177
3104
  # ex. ["San Diego", "Los Angeles", "San Juan"]
3178
3105
 
3179
3106
  # For pointer fields, now returns object IDs by default (v1.12.0+)
3180
- Asset.distinct(:author_team)
3107
+ Document.distinct(:author_workspace)
3181
3108
  # => ["team1", "team2", "team3"] # Just the object IDs
3182
3109
 
3183
3110
  # Pre-v1.12.0 behavior returned full pointer hashes:
3184
- # [{"__type"=>"Pointer", "className"=>"Team", "objectId"=>"team1"}, ...]
3111
+ # [{"__type"=>"Pointer", "className"=>"Workspace", "objectId"=>"team1"}, ...]
3185
3112
 
3186
3113
  # To get Parse::Pointer objects in v1.12.0+
3187
- Asset.distinct(:author_team, return_pointers: true)
3188
- # => [#<Parse::Pointer @parse_class="Team" @id="team1">, ...]
3114
+ Document.distinct(:author_workspace, return_pointers: true)
3115
+ # => [#<Parse::Pointer @parse_class="Workspace" @id="team1">, ...]
3189
3116
 
3190
3117
  # same using query instance
3191
3118
  query = Parse::Query.new("_User")
@@ -3195,7 +3122,7 @@ view and returns the results in an array. You may mix this with additional query
3195
3122
  ```
3196
3123
 
3197
3124
  ### Query Expressions
3198
- The set of supported expressions based on what is available through the Parse REST API. _For those who don't prefer the DataMapper style syntax, we have provided method accessors for each of the expressions._ A full description of supported query operations, please refer to the [`Parse::Query`](https://www.modernistik.com/gems/parse-stack/Parse/Query.html) API reference.
3125
+ The set of supported expressions based on what is available through the Parse REST API. _For those who don't prefer the DataMapper style syntax, we have provided method accessors for each of the expressions._ A full description of supported query operations, please refer to the [`Parse::Query`](https://neurosynq.github.io/parse-stack-next/Parse/Query.html) API reference.
3199
3126
 
3200
3127
  #### :order
3201
3128
  Specify a field to sort by.
@@ -3368,6 +3295,8 @@ A true/false value. If you provided a master key as part of `Parse.setup()`, it
3368
3295
  Song.all limit: 3, use_master_key: false
3369
3296
  ```
3370
3297
 
3298
+ As of v5.0, `Parse::Query` initializes `@use_master_key` to `nil` (tri-state: "no caller preference") rather than `true`. Server-mode behavior is unchanged — when nothing in the call chain expresses a preference, the request layer still sends the master key. The difference matters for `Parse.client_mode = true` processes and inside `Parse.with_session(user) { … }` blocks: the previous `true` default short-circuited those resolutions and silently master-key-stamped queries. Explicitly passing `use_master_key: true` (or calling `query.use_master_key = true`) still forces the header. `Parse::Query#assert_mongo_direct_routable!` treats a configured master key on the client as an ambient credential in server mode: direct-only constraints (Atlas Search-shaped operators, etc.) route through the mongo-direct path as long as `Parse.client_mode` is false and `use_master_key` was not explicitly set to `false`. The gate still raises `Parse::Query::MongoDirectRequired` for client-mode processes or queries that explicitly opt out of the master key without supplying a `session_token` / `.scope_to_user(user)` / `.scope_to_role(role)`.
3299
+
3371
3300
  #### :session
3372
3301
  This will make sure that the query is performed on behalf (and with the privileges) of an authenticated user which will cause record ACLs to be enforced. If a session token is provided, caching will be disabled for this request. You may pass a string representing the session token, an authenticated `Parse::User` instance or a `Parse::Session` instance.
3373
3302
 
@@ -3387,7 +3316,7 @@ The `where` clause is based on utilizing a set of constraints on the defined col
3387
3316
  { :column.constraint => value }
3388
3317
  ```
3389
3318
 
3390
- ## [Query Constraints](https://www.modernistik.com/gems/parse-stack/Parse/Constraint.html)
3319
+ ## [Query Constraints](https://neurosynq.github.io/parse-stack-next/Parse/Constraint.html)
3391
3320
  Most of the constraints supported by Parse are available to `Parse::Query`. Assuming you have a column named `field`, here are some examples. For an explanation of the constraints, please see [Parse Query Constraints documentation](http://docs.parseplatform.org/rest/guide/#queries). You can build your own custom query constraints by creating a `Parse::Constraint` subclass. For all these `where` clauses assume `q` is a `Parse::Query` object.
3392
3321
 
3393
3322
  #### Equals
@@ -3810,7 +3739,7 @@ songs = find_songs Artist.pointer(artist_id)
3810
3739
 
3811
3740
  ```
3812
3741
 
3813
- ### [Geo Queries](https://www.modernistik.com/gems/parse-stack/Parse/Constraint/NearSphereQueryConstraint.html)
3742
+ ### [Geo Queries](https://neurosynq.github.io/parse-stack-next/Parse/Constraint/NearSphereQueryConstraint.html)
3814
3743
  Equivalent to the `$nearSphere` Parse query operation. This is only applicable if the field is of type `GeoPoint`. This will query Parse and return a list of results ordered by distance with the nearest object being first.
3815
3744
 
3816
3745
  ```ruby
@@ -3836,7 +3765,7 @@ PlaceObject.all :location.near => geopoint.max_miles(10)
3836
3765
 
3837
3766
  We will support `$maxDistanceInKilometers` (for kms) and `$maxDistanceInRadians` (for radian angle) in the future.
3838
3767
 
3839
- #### [Bounding Box Constraint](https://www.modernistik.com/gems/parse-stack/Parse/Constraint/WithinGeoBoxQueryConstraint.html)
3768
+ #### [Bounding Box Constraint](https://neurosynq.github.io/parse-stack-next/Parse/Constraint/WithinGeoBoxQueryConstraint.html)
3840
3769
  Equivalent to the `$within` Parse query operation and `$box` geopoint constraint. The rectangular bounding box is defined by a southwest point as the first parameter, followed by the a northeast point. Please note that Geo box queries that cross the international date lines are not currently supported by Parse.
3841
3770
 
3842
3771
  ```ruby
@@ -3851,7 +3780,7 @@ ne = Parse::GeoPoint.new 36.12, -115.31 # Las Vegas
3851
3780
  PlaceObject.all :location.within_box => [sw,ne]
3852
3781
  ```
3853
3782
 
3854
- #### [Polygon Area Constraint](https://www.modernistik.com/gems/parse-stack/Parse/Constraint/WithinPolygonQueryConstraint.html)
3783
+ #### [Polygon Area Constraint](https://neurosynq.github.io/parse-stack-next/Parse/Constraint/WithinPolygonQueryConstraint.html)
3855
3784
  Equivalent to the `$geoWithin` Parse query operation and `$polygon` geopoint constraint. The polygon area is described by a list of `Parse::GeoPoint` objects and should contain 3 or more points. This feature is only available in Parse-Server version 2.4.2 and later.
3856
3785
 
3857
3786
  ```ruby
@@ -3867,7 +3796,7 @@ Equivalent to the `$geoWithin` Parse query operation and `$polygon` geopoint con
3867
3796
  SunkenShip.all :location.within_polygon => [bermuda, san_juan, miami]
3868
3797
  ```
3869
3798
 
3870
- #### [Full Text Search Constraint](https://www.modernistik.com/gems/parse-stack/Parse/Constraint/FullTextSearchQueryConstraint.html)
3799
+ #### [Full Text Search Constraint](https://neurosynq.github.io/parse-stack-next/Parse/Constraint/FullTextSearchQueryConstraint.html)
3871
3800
  Equivalent to the `$text` Parse query operation and `$search` parameter constraint for efficient search capabilities. By creating indexes on one or more columns your strings are turned into tokens for full text search functionality. The `$search` key can take any number of parameters in hash form. *Requires Parse Server 2.5.0 or later*
3872
3801
 
3873
3802
  ```ruby
@@ -4268,7 +4197,7 @@ Parse::Push.new
4268
4197
  Parse::Push.new
4269
4198
  .to_channels("sports", "alerts")
4270
4199
  .with_title("Game Alert")
4271
- .with_body("Your team is playing now!")
4200
+ .with_body("Your workspace is playing now!")
4272
4201
  .with_badge(1)
4273
4202
  .with_sound("alert.caf")
4274
4203
  .with_data(game_id: "12345", action: "open_game")
@@ -4424,9 +4353,29 @@ push.data = { uri: "app://deep_link_path" }
4424
4353
  push.send
4425
4354
  ```
4426
4355
 
4427
- ## AI Agent Integration (Experimental)
4356
+ ## Analytics
4357
+
4358
+ `Parse.track_event(name, dimensions: {}, **opts)` (v5.0+) is the top-level shortcut for sending events to Parse Server's `POST /events/<name>` endpoint. The dimensions hash MUST be passed via the `dimensions:` keyword — under Ruby 3 kwarg separation, loose symbol arguments are absorbed by `**opts` and never reach the POST body. The event name is validated against `[\w\-\.]` at the SDK boundary so the value cannot escape the `/events/` path segment.
4359
+
4360
+ ```ruby
4361
+ # Server-side, master-key in process
4362
+ Parse.track_event("post_viewed", dimensions: { source: "feed", workspace: "w1" })
4363
+
4364
+ # No dimensions
4365
+ Parse.track_event("AppOpened")
4366
+
4367
+ # Client-mode, scoped to a session
4368
+ Parse.track_event("search",
4369
+ dimensions: { query: "tabby cats" },
4370
+ session_token: user.session_token, use_master_key: false,
4371
+ )
4372
+ ```
4373
+
4374
+ Parse Server's default `analyticsAdapter` is a no-op: events POST'd are accepted (HTTP 200) but are not persisted and cannot be read back through the SDK. Operators who wire in a custom adapter decide what (if anything) to do with each event. The legacy parse.com eight-dimension cap does NOT apply to Parse Server out of the box. If you need to query analytics events from the SDK, persist them to a regular `Parse::Object` subclass instead. The underlying request is a blocking HTTP POST — wrap in a thread or background job if you do not want it on the request path.
4375
+
4376
+ ## AI Agent Integration
4428
4377
 
4429
- Parse Stack includes experimental support for AI/LLM agents to interact with your Parse data through a standardized tool interface. This enables natural language querying and intelligent data exploration.
4378
+ Parse Stack includes first-class support for AI/LLM agents to interact with your Parse data through a standardized tool interface, including an MCP 2025-06-18 Streamable HTTP transport. This enables natural language querying and intelligent data exploration with rate limiting, prompt-injection protection, and per-agent ACL scoping.
4430
4379
 
4431
4380
  ### Basic Usage
4432
4381
 
@@ -4468,6 +4417,28 @@ agent = Parse::Agent.new(permissions: :write)
4468
4417
  agent = Parse::Agent.new(permissions: :admin)
4469
4418
  ```
4470
4419
 
4420
+ ### Client Mode (v5.0)
4421
+
4422
+ When `Parse::Agent` is constructed against a `Parse::Client` that carries no master key and a non-empty `session_token:`, it switches to *client mode*. The dispatch ceiling is a small allowlist of session-token REST tools (`list_tools`, `get_object`, `get_objects`, `query_class`, `count_objects`, `get_sample_objects`, and — gated by `allow_mutations:` — `create_object`, `update_object`, `delete_object`). Aggregate, atlas-search, schema-introspection, explain, and generic `call_method` are refused because they require either the master key or a direct MongoDB connection.
4423
+
4424
+ ```ruby
4425
+ # An unprivileged client + a user's session token
4426
+ Parse.setup(server_url: "...", application_id: "...", api_key: "...") # no master_key
4427
+ agent = Parse::Agent.new(session_token: user.session_token)
4428
+
4429
+ agent.client_mode? # => true
4430
+ agent.allow_mutations? # => false (default in client mode)
4431
+
4432
+ # Read tools work; Parse Server enforces ACL + CLP + protectedFields natively.
4433
+ agent.execute(:query_class, class_name: "Post", limit: 10)
4434
+
4435
+ # Mutations are opt-in per agent:
4436
+ writer = Parse::Agent.new(session_token: user.session_token, allow_mutations: true)
4437
+ writer.execute(:create_object, class_name: "Post", fields: { title: "Hi" })
4438
+ ```
4439
+
4440
+ Custom tools default to master-key-only. Mark a registered tool eligible for client mode with `Parse::Agent::Tools.register(:my_tool, ..., client_safe: true)`; the handler is then responsible for routing through `agent.client` with `agent.session_token` (never the master key). Sub-agents cannot widen the parent's `allow_mutations:` gate.
4441
+
4471
4442
  ### Agent Metadata DSL
4472
4443
 
4473
4444
  Annotate your models with agent-friendly metadata:
@@ -4479,12 +4450,12 @@ class Song < Parse::Object
4479
4450
 
4480
4451
  property :title, :string, _description: "The song title"
4481
4452
  property :plays, :integer, _description: "Total play count"
4482
- property :is_removed, :boolean
4453
+ property :archived, :boolean
4483
4454
 
4484
4455
  # Per-class "valid state" predicate applied by default on every read tool
4485
4456
  # (query_class, count_objects, aggregate). Opt out per-call with
4486
4457
  # `apply_canonical_filter: false`.
4487
- agent_canonical_filter "isRemoved" => { "$ne" => true }
4458
+ agent_canonical_filter "archived" => { "$ne" => true }
4488
4459
 
4489
4460
  # Expose methods with permission levels
4490
4461
  agent_readonly :find_popular, "Find songs with high play counts"
@@ -5175,7 +5146,7 @@ Song.query.writable_by_role(["Admin", "Editor"]).results(mongo_direct: true) #
5175
5146
 
5176
5147
  Parse-Stack provides intelligent dirty tracking for ACL objects, correctly handling in-place modifications and content comparison.
5177
5148
 
5178
- **`acl_was` Captures Original State:**
5149
+ **`acl_was` Posts Original State:**
5179
5150
 
5180
5151
  When modifying an ACL in place (via `apply`, `apply_role`, etc.), `acl_was` correctly returns the state *before* any modifications:
5181
5152
 
@@ -5201,19 +5172,19 @@ obj.acl_changed? # => true
5201
5172
  Setting an ACL to identical values does not mark the object as dirty:
5202
5173
 
5203
5174
  ```ruby
5204
- membership = Membership.find(id)
5205
- membership.clear_changes!
5175
+ subscription = Subscription.find(id)
5176
+ subscription.clear_changes!
5206
5177
 
5207
5178
  # Rebuild ACL to the same values (common in before_save hooks)
5208
- membership.acl = Parse::ACL.new
5209
- membership.acl.apply(:public, true, false)
5210
- membership.acl.apply_role("Admin", true, true)
5179
+ subscription.acl = Parse::ACL.new
5180
+ subscription.acl.apply(:public, true, false)
5181
+ subscription.acl.apply_role("Admin", true, true)
5211
5182
  # ... same permissions as before ...
5212
5183
 
5213
5184
  # If content is identical, object is NOT dirty
5214
- membership.acl_changed? # => false
5215
- membership.dirty? # => false
5216
- membership.save # No unnecessary server request
5185
+ subscription.acl_changed? # => false
5186
+ subscription.dirty? # => false
5187
+ subscription.save # No unnecessary server request
5217
5188
  ```
5218
5189
 
5219
5190
  **New Objects:**