parse-stack-next 5.2.1 → 5.4.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/.bundle/config +1 -0
- data/.gitignore +2 -0
- data/CHANGELOG.md +616 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +12 -4
- data/README.md +296 -3
- data/Rakefile +243 -41
- data/docs/atlas_vector_search_guide.md +86 -2
- data/docs/client_sdk_guide.md +38 -0
- data/docs/mcp_guide.md +119 -4
- data/docs/mongodb_direct_guide.md +93 -1
- data/docs/usage_guide.md +11 -1
- data/docs/webhooks_guide.md +418 -0
- data/examples/README.md +46 -0
- data/examples/basic_client.rb +93 -0
- data/examples/basic_server.rb +109 -0
- data/examples/live_query_listener.rb +98 -0
- data/examples/rag_chatbot.rb +221 -0
- data/examples/webhook_server.rb +111 -0
- data/lib/parse/agent/mcp_rack_app.rb +285 -62
- data/lib/parse/agent/tools.rb +45 -5
- data/lib/parse/api/aggregate.rb +7 -1
- data/lib/parse/api/cloud_functions.rb +12 -4
- data/lib/parse/api/hooks.rb +46 -9
- data/lib/parse/api/objects.rb +16 -2
- data/lib/parse/api/path_segment.rb +33 -0
- data/lib/parse/api/server.rb +94 -0
- data/lib/parse/api/users.rb +58 -2
- data/lib/parse/atlas_search.rb +7 -7
- data/lib/parse/client/body_builder.rb +5 -0
- data/lib/parse/client/protocol.rb +4 -0
- data/lib/parse/client.rb +174 -9
- data/lib/parse/embeddings/spend_cap.rb +255 -0
- data/lib/parse/embeddings.rb +1 -0
- data/lib/parse/live_query/client.rb +3 -1
- data/lib/parse/live_query/subscription.rb +32 -5
- data/lib/parse/model/acl.rb +4 -2
- data/lib/parse/model/associations/belongs_to.rb +47 -0
- data/lib/parse/model/classes/audience.rb +52 -4
- data/lib/parse/model/classes/user.rb +200 -3
- data/lib/parse/model/core/embed_managed.rb +113 -0
- data/lib/parse/model/core/pluralized_aliases.rb +30 -0
- data/lib/parse/model/core/properties.rb +27 -0
- data/lib/parse/model/core/querying.rb +73 -1
- data/lib/parse/model/core/vector_searchable.rb +161 -0
- data/lib/parse/model/file.rb +35 -2
- data/lib/parse/model/object.rb +28 -5
- data/lib/parse/mongodb.rb +7 -1
- data/lib/parse/pipeline_security.rb +5 -3
- data/lib/parse/query/constraints.rb +29 -0
- data/lib/parse/query.rb +265 -27
- data/lib/parse/retrieval/agent_tool.rb +49 -0
- data/lib/parse/retrieval/reranker/cohere.rb +218 -0
- data/lib/parse/retrieval/reranker.rb +157 -0
- data/lib/parse/retrieval/retriever.rb +110 -23
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +173 -1
- data/lib/parse/two_factor_auth/user_extension.rb +123 -31
- data/lib/parse/vector_search/hybrid.rb +578 -0
- data/lib/parse/webhooks/payload.rb +399 -11
- data/lib/parse/webhooks/trigger_audit.rb +502 -0
- data/lib/parse/webhooks.rb +215 -3
- data/scripts/docker/Dockerfile.parse +5 -1
- data/scripts/docker/docker-compose.test.yml +31 -0
- data/scripts/docker/docker-compose.verifyemail.yml +4 -0
- data/scripts/docker/preflight.sh +76 -0
- data/scripts/start-parse.sh +52 -4
- metadata +16 -1
data/Gemfile
CHANGED
|
@@ -32,5 +32,12 @@ group :test, :development do
|
|
|
32
32
|
gem "puma"
|
|
33
33
|
gem "sinatra"
|
|
34
34
|
gem "rack-test"
|
|
35
|
+
# MFA / TOTP test infrastructure (Parse::MFA, two_factor_auth).
|
|
36
|
+
# rotp: generates TOTP secrets and time-based codes so the MFA unit and
|
|
37
|
+
# integration tests can enroll and log in against Parse Server's
|
|
38
|
+
# TOTP adapter (SHA1 / 6 digits / 30s — rotp's defaults match).
|
|
39
|
+
# rqrcode: renders the provisioning QR code exercised by Parse::MFA.qr_code.
|
|
40
|
+
gem "rotp"
|
|
41
|
+
gem "rqrcode"
|
|
35
42
|
# gem "thin" # for yard server - disabled due to eventmachine compilation issues
|
|
36
43
|
end
|
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.4.0)
|
|
5
5
|
activemodel (>= 6.1, < 9)
|
|
6
6
|
activesupport (>= 6.1, < 9)
|
|
7
7
|
connection_pool (>= 2.2, < 4)
|
|
@@ -39,6 +39,7 @@ GEM
|
|
|
39
39
|
bundler-audit (0.9.3)
|
|
40
40
|
bundler (>= 1.2.0)
|
|
41
41
|
thor (~> 1.0)
|
|
42
|
+
chunky_png (1.4.0)
|
|
42
43
|
coderay (1.1.3)
|
|
43
44
|
concurrent-ruby (1.3.6)
|
|
44
45
|
connection_pool (3.0.2)
|
|
@@ -54,7 +55,7 @@ GEM
|
|
|
54
55
|
faraday-net_http (>= 2.0, < 3.5)
|
|
55
56
|
json
|
|
56
57
|
logger
|
|
57
|
-
faraday-net_http (3.4.
|
|
58
|
+
faraday-net_http (3.4.4)
|
|
58
59
|
net-http (~> 0.5)
|
|
59
60
|
faraday-net_http_persistent (2.3.1)
|
|
60
61
|
faraday (~> 2.5)
|
|
@@ -72,7 +73,7 @@ GEM
|
|
|
72
73
|
prism (>= 1.3.0)
|
|
73
74
|
rdoc (>= 4.0.0)
|
|
74
75
|
reline (>= 0.4.2)
|
|
75
|
-
json (2.19.
|
|
76
|
+
json (2.19.8)
|
|
76
77
|
logger (1.7.0)
|
|
77
78
|
method_source (1.1.0)
|
|
78
79
|
minitest (6.0.6)
|
|
@@ -104,7 +105,7 @@ GEM
|
|
|
104
105
|
coderay (~> 1.1)
|
|
105
106
|
method_source (~> 1.0)
|
|
106
107
|
reline (>= 0.6.0)
|
|
107
|
-
psych (5.
|
|
108
|
+
psych (5.4.0)
|
|
108
109
|
date
|
|
109
110
|
stringio
|
|
110
111
|
puma (8.0.2)
|
|
@@ -133,6 +134,11 @@ GEM
|
|
|
133
134
|
connection_pool
|
|
134
135
|
reline (0.6.3)
|
|
135
136
|
io-console (~> 0.5)
|
|
137
|
+
rotp (6.3.0)
|
|
138
|
+
rqrcode (3.2.0)
|
|
139
|
+
chunky_png (~> 1.0)
|
|
140
|
+
rqrcode_core (~> 2.0)
|
|
141
|
+
rqrcode_core (2.1.0)
|
|
136
142
|
ruby-progressbar (1.13.0)
|
|
137
143
|
rufo (0.18.2)
|
|
138
144
|
securerandom (0.4.1)
|
|
@@ -181,6 +187,8 @@ DEPENDENCIES
|
|
|
181
187
|
rake
|
|
182
188
|
redcarpet
|
|
183
189
|
redis
|
|
190
|
+
rotp
|
|
191
|
+
rqrcode
|
|
184
192
|
rufo
|
|
185
193
|
sinatra
|
|
186
194
|
webrick
|
data/README.md
CHANGED
|
@@ -4,6 +4,21 @@
|
|
|
4
4
|
|
|
5
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
|
+
### What's new in 5.4
|
|
8
|
+
|
|
9
|
+
- **5.4.0 — Hybrid search + reranking for RAG** — `Class.hybrid_search(text:, lexical:, vector:, k:, fusion:)` fuses a lexical Atlas Search branch with a `$vectorSearch` branch using reciprocal-rank fusion (RRF): lexical search nails exact tokens (codes, proper nouns), vector search nails paraphrase, and fusing the two beats either alone. Each branch enforces ACL/CLP independently before fusion (no separate hydration fetch to secure); results carry `#hybrid_score` / `#hybrid_ranks`. `Parse::VectorSearch::Hybrid.rank_fusion_supported?` detects Atlas 8.0+ native `$rankFusion` by a cached behavioural probe (native execution is opt-in; client-side RRF is the always-enforced default). `Parse::Retrieval::Reranker` adds cross-encoder reranking (`Reranker::Cohere` over `/v2/rerank`, plus a deterministic `Reranker::Fixture`), wired into `Parse::Retrieval.retrieve(hybrid:, rerank:)`. `Parse::Embeddings::SpendCap` adds an opt-in per-tenant embedding token cap (hard-refuse) at the `semantic_search` agent-tool boundary. See [CHANGELOG.md](./CHANGELOG.md) and [`docs/atlas_vector_search_guide.md`](./docs/atlas_vector_search_guide.md)
|
|
10
|
+
- **5.4.0 — Vector backfill, visibility, and webhook redaction** — `Class.embed_pending!` backfills embeddings for records whose managed `:vector` field is null (objectId-cursor pagination); `Parse::Object#compute_embedding!` forces an in-place recompute without a save; `vector_visibility :owner_only | :public` controls whether a class's vectors appear in `as_json` by default; and webhook trigger payloads now strip declared `:vector` columns by default (a `:public` class keeps them). See [CHANGELOG.md](./CHANGELOG.md)
|
|
11
|
+
- **5.4.0 — TOTP multi-factor auth works end to end** — the `Parse::User` MFA lifecycle is now fully functional and exercised against a real MFA-enabled Parse Server. `setup_mfa!(secret:, token:)` enrolls TOTP and returns recovery codes; `Parse::User.login_with_mfa(user, pass, code)` completes a second-factor login; `mfa_enabled?` / `mfa_status` report enrollment after an ordinary fetch — the SDK strips the raw TOTP secret and recovery codes that Parse Server returns in `authData` but preserves a leak-safe `{status: "enabled"}` projection so the status reads correctly without exposing the secret; `disable_mfa!(current_token:)` turns MFA off after re-validating the current code (a wrong code raises `Parse::MFA::VerificationError`), and `disable_mfa_master_key!(authorized_by:)` is the operator override. Each MFA write also no longer raises an internal argument error before reaching the server. Interactively, `rake client:console` now prompts for a TOTP / recovery code (or reads `PARSE_LOGIN_MFA`) when logging into an enrolled account. See [CHANGELOG.md](./CHANGELOG.md)
|
|
12
|
+
- **5.4.0 — Request email-address verification** — `Parse::User.request_email_verification(email)` and the instance `Parse::User#request_email_verification` ask Parse Server to (re)send the verification email for a registered, not-yet-verified user, mirroring `request_password_reset` (per-email rate limiting, Boolean return). Requires a server email adapter with `verifyUserEmails` enabled. See [CHANGELOG.md](./CHANGELOG.md)
|
|
13
|
+
- **5.4.0 — Audience hash queries persist correctly** — `Parse::Audience#query` is now stored as a JSON string on the wire to match Parse Server's `_Audience.query` column type, so saving an audience with a `Hash` query no longer fails the server schema check. The public API is unchanged — assign a `Hash`, read a `Hash` back. See [CHANGELOG.md](./CHANGELOG.md)
|
|
14
|
+
- **5.4.0 — Faster AtlasSearch role-cache expiry** — `Parse::AtlasSearch` `role_cache_ttl` now defaults to 30 seconds (was 120) so a role grant or revoke is reflected in `$search` ACL decisions sooner, at the cost of slightly more frequent role lookups. See [CHANGELOG.md](./CHANGELOG.md)
|
|
15
|
+
|
|
16
|
+
### What's new in 5.3
|
|
17
|
+
|
|
18
|
+
- **5.3.0 — Run webhook handlers (and clients) as the calling user** — Parse Server embeds the caller's live session token in every trigger webhook fired by a logged-in user. A handler can now opt in to acting on the server *as that user* — full ACL/CLP/`protectedFields` enforcement, no master key. `payload.session_token` exposes the captured token (`nil` for master-key requests; still scrubbed from `payload.user`/`payload.object`/`as_json`/logs); `payload.user_agent` returns a client-mode `Parse::Agent`, and `payload.user_client` a non-master `Parse::Client` with the token **bound** so even raw REST calls authorize as the user. The same user-scoped client is available client-side via `Parse::User#session_client` and the `Parse::Client#become(token)` primitive, with `Parse::Client#with_session { … }` for block scoping. Backed by a new `Parse::Client.new(session_token:)` option. See [Acting as the calling user](#acting-as-the-calling-user)
|
|
19
|
+
- **5.3.0 — Pluralized class-name aliases** — referencing the plural form of a model constant now resolves to that class, so `Posts.where(:author.eq => user).count` works for a class `Post`. The alias is created lazily on first reference and is the *same class object*, so every class method (`query`/`where`, `count`, `find`, `all`, scopes) works through it and `Posts.parse_class` still returns `"Post"`. Because it is the same class it adds no `Parse::Object.descendants` entry and never registers a separate Parse schema class. Classes whose name already ends in `s` are skipped by the automatic path; non-Parse plurals and typos fall through to a normal `NameError`. On by default — opt out with `Parse.pluralized_aliases = false` (or `PARSE_PLURALIZED_ALIASES=false`). For a custom plural, an `s`-ending class, or a namespaced model, call `pluralized_alias!` in the class body. See [Pluralized class-name aliases](#pluralized-class-name-aliases)
|
|
20
|
+
- **5.3.0 — afterSave create reports changed fields; force_ssl-consistent file equality** — a trigger handler that keys off dirty tracking now sees every field on an `afterSave` *create*, symmetric with `afterSave` updates: the built object marks each populated data property changed (from `nil`) while `createdAt`/`updatedAt`/`ACL`/`objectId` stay clean and object readability, `new?`, and `existed?` are unchanged — so a handler that builds a payload from `*_changed?` / `changes` works uniformly across create and update. Separately, `Parse::File#==` now compares both files through the canonical `url` reader, so two files at the same location compare equal regardless of `Parse::File.force_ssl` (and `a == b` matches `b == a`), and a re-signed URL for the same object no longer reads as a change. See [Cloud Code Triggers](#cloud-code-triggers)
|
|
21
|
+
|
|
7
22
|
### What's new in 5.2
|
|
8
23
|
|
|
9
24
|
- **5.2.1 — Webhook triggers receive the full Parse object** — trigger handlers (`beforeSave`/`afterSave`/…) now get the complete server object (`createdAt`/`updatedAt`, `ACL`, internal fields); only live credentials (session tokens, password hashes) are stripped. `Parse::Object#existed?` / `#new?` are reliable in `afterSave`, `afterSave` updates carry dirty tracking, and the model lifecycle runs in ActiveModel order — `before_save → before_create` then `after_create → after_save` — so `before_create` now fires for REST/JS/Auth0 creates (and `after_save` no longer double-fires). See [Cloud Code Triggers](#cloud-code-triggers)
|
|
@@ -203,9 +218,20 @@ result = Parse.call_function :myFunctionName, {param: value}
|
|
|
203
218
|
|
|
204
219
|
```
|
|
205
220
|
|
|
221
|
+
## Examples
|
|
222
|
+
|
|
223
|
+
Runnable, self-contained scripts live in [`examples/`](examples/) — see
|
|
224
|
+
[`examples/README.md`](examples/README.md) for the full index. Highlights:
|
|
225
|
+
|
|
226
|
+
- [`basic_server.rb`](examples/basic_server.rb) — master-key setup: models, schema, CRUD + queries.
|
|
227
|
+
- [`basic_client.rb`](examples/basic_client.rb) — unprivileged client with row-level ACL enforcement.
|
|
228
|
+
- [`live_query_listener.rb`](examples/live_query_listener.rb) — interactive LiveQuery console scoped to a user's session.
|
|
229
|
+
- [`rag_chatbot.rb`](examples/rag_chatbot.rb) — retrieval-augmented generation with `semantic_search` + an OpenAI/Anthropic add-in.
|
|
230
|
+
- [`transaction_example.rb`](examples/transaction_example.rb) — atomic multi-object transactions.
|
|
231
|
+
|
|
206
232
|
## Release History
|
|
207
233
|
|
|
208
|
-
**Current version: 5.0
|
|
234
|
+
**Current version: 5.4.0** | **Ruby 3.2+ required**
|
|
209
235
|
|
|
210
236
|
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.
|
|
211
237
|
|
|
@@ -296,6 +322,7 @@ The 1.x line is the original [`modernistik/parse-stack`](https://github.com/mode
|
|
|
296
322
|
- [Linking and Unlinking](#linking-and-unlinking)
|
|
297
323
|
- [Request Password Reset](#request-password-reset)
|
|
298
324
|
- [Modeling and Subclassing](#modeling-and-subclassing)
|
|
325
|
+
- [Pluralized class-name aliases](#pluralized-class-name-aliases)
|
|
299
326
|
- [Defining Properties](#defining-properties)
|
|
300
327
|
- [Accessor Aliasing](#accessor-aliasing)
|
|
301
328
|
- [Property Options](#property-options)
|
|
@@ -1579,8 +1606,11 @@ user.mfa_status # => :enabled, :disabled, or :unknown
|
|
|
1579
1606
|
# Disable MFA (requires current token)
|
|
1580
1607
|
user.disable_mfa!(current_token: "123456")
|
|
1581
1608
|
|
|
1582
|
-
# Admin reset (master key) —
|
|
1583
|
-
|
|
1609
|
+
# Admin reset (master key) — fails closed: pass either an admin_role:
|
|
1610
|
+
# for the library to verify, or allow_unverified: true to assert that
|
|
1611
|
+
# you have already authorized the operator out-of-band.
|
|
1612
|
+
user.disable_mfa_master_key!(authorized_by: admin_user, admin_role: "Admin")
|
|
1613
|
+
# or: user.disable_mfa_master_key!(authorized_by: admin_user, allow_unverified: true)
|
|
1584
1614
|
```
|
|
1585
1615
|
|
|
1586
1616
|
**SMS MFA (requires Parse Server SMS callback):**
|
|
@@ -1622,6 +1652,61 @@ class Commentary < Parse::Object
|
|
|
1622
1652
|
end
|
|
1623
1653
|
```
|
|
1624
1654
|
|
|
1655
|
+
### Pluralized class-name aliases
|
|
1656
|
+
|
|
1657
|
+
As a convenience, the plural form of a model constant resolves to that class, so the plural reads naturally at a query call site:
|
|
1658
|
+
|
|
1659
|
+
```ruby
|
|
1660
|
+
class Post < Parse::Object
|
|
1661
|
+
property :title, :string
|
|
1662
|
+
belongs_to :author, as: :user
|
|
1663
|
+
end
|
|
1664
|
+
|
|
1665
|
+
# `Posts` resolves to `Post` on first reference:
|
|
1666
|
+
Posts.where(:author.eq => current_user).count
|
|
1667
|
+
Posts.query(:title.exists => true).results
|
|
1668
|
+
Posts.find("abc123")
|
|
1669
|
+
```
|
|
1670
|
+
|
|
1671
|
+
The alias is created lazily (via `const_missing`) the first time the plural constant is referenced, and it is the **same class object** as the singular — `Posts.equal?(Post)` is `true`. As a result:
|
|
1672
|
+
|
|
1673
|
+
- Every class method works through it for free: `query`/`where`, `count`, `find`, `all`, and any `scope` you define.
|
|
1674
|
+
- `Posts.parse_class` still returns `"Post"`. The alias is purely a Ruby-constant convenience.
|
|
1675
|
+
- It adds no `Parse::Object.descendants` entry and **never registers a separate Parse schema class** — schema introspection and `Parse.auto_upgrade!` see exactly one class.
|
|
1676
|
+
|
|
1677
|
+
The automatic path is deliberately conservative:
|
|
1678
|
+
|
|
1679
|
+
- A class whose name already ends in `s` (for example `Status`, `Series`) is **skipped**, since its plural is ambiguous.
|
|
1680
|
+
- A plural that does not singularize to a known `Parse::Object` subclass (a typo, or `Strings` → `String`) falls through to a normal `NameError` — the SDK does not change Ruby's behavior for non-model constants.
|
|
1681
|
+
|
|
1682
|
+
The feature is enabled by default. Opt out globally:
|
|
1683
|
+
|
|
1684
|
+
```ruby
|
|
1685
|
+
Parse.pluralized_aliases = false # programmatic
|
|
1686
|
+
# or
|
|
1687
|
+
# PARSE_PLURALIZED_ALIASES=false # environment
|
|
1688
|
+
```
|
|
1689
|
+
|
|
1690
|
+
For a custom plural, a class whose name ends in `s` that you *do* want aliased, or a namespaced model (where the alias should live on the enclosing module rather than at the top level), declare it explicitly with the `pluralized_alias!` macro. The explicit macro ignores the global flag and the `s`-ending guard:
|
|
1691
|
+
|
|
1692
|
+
```ruby
|
|
1693
|
+
class Status < Parse::Object
|
|
1694
|
+
pluralized_alias! :Statuses # defines ::Statuses => Status
|
|
1695
|
+
end
|
|
1696
|
+
|
|
1697
|
+
module Blog
|
|
1698
|
+
class Article < Parse::Object
|
|
1699
|
+
pluralized_alias! # defines Blog::Articles => Blog::Article
|
|
1700
|
+
end
|
|
1701
|
+
end
|
|
1702
|
+
```
|
|
1703
|
+
|
|
1704
|
+
If the target constant already exists and is not the class itself, `pluralized_alias!` raises `ArgumentError` rather than clobbering it (a code reloader swapping the class object is detected and the alias is re-pointed instead).
|
|
1705
|
+
|
|
1706
|
+
The automatic path and the macro differ in *where* the alias constant is installed. The macro always anchors it on the class's own parent — top level for `Post`, the enclosing module for a namespaced model. The automatic path installs it on the module where the plural was first referenced (Ruby's `const_missing` fires on the referencing lexical scope), so a bare `Posts` written inside `module Blog` defines `Blog::Posts`. Both resolve to the same class; if you want a single, predictable top-level constant, prefer the macro.
|
|
1707
|
+
|
|
1708
|
+
> **Note on reloading:** under a code reloader (for example Zeitwerk in Rails development), a model class is swapped for a fresh object on reload, but the alias constant the SDK created is not tracked by the reloader and is therefore not removed. The `pluralized_alias!` macro detects this on the next run of the class body and re-points the alias to the current class. An *automatically*-created alias, however, stays bound to the previous class object until the process restarts (`const_missing` cannot re-fire for a constant that is still defined). If you depend on the plural during development and want fully deterministic reload behavior, declare it explicitly with `pluralized_alias!`, or disable the feature with `Parse.pluralized_aliases = false`.
|
|
1709
|
+
|
|
1625
1710
|
### Defining Properties
|
|
1626
1711
|
Properties are considered a literal-type of association. This means that a defined local property maps directly to a column name for that remote Parse class which contain the value. **All properties are implicitly formatted to map to a lower-first camelcase version in Parse (remote).** Therefore a local property defined as `like_count`, would be mapped to the remote column of `likeCount` automatically. The only special behavior to this rule is the `:id` property which maps to `objectId` in Parse. This implicit conversion mapping is the default behavior, but can be changed on a per-property basis. All Parse data types are supported and all Parse::Object subclasses already provide definitions for `:id` (objectId), `:created_at` (createdAt), `:updated_at` (updatedAt) and `:acl` (ACL) properties.
|
|
1627
1712
|
|
|
@@ -4855,6 +4940,32 @@ The `parse_object` handed to your handler is the **full object as Parse Server s
|
|
|
4855
4940
|
|
|
4856
4941
|
For any `after_*` hook, return values are not needed since Parse does not utilize them. You may also register as many `after_save` or `after_delete` handlers as you prefer, all of them will be called.
|
|
4857
4942
|
|
|
4943
|
+
For `before_save` (and functions), the handler's value **is** the response Parse Server acts on — return the (possibly mutated) `parse_object` to allow the write, or `false` / `error!` to reject it. You can set that value with an explicit `return` or as the block's last expression; both work, as do the proc idioms `next value` / `break value`:
|
|
4944
|
+
|
|
4945
|
+
```ruby
|
|
4946
|
+
Parse::Webhooks.route :before_save, :Artist do
|
|
4947
|
+
artist = parse_object
|
|
4948
|
+
return artist if artist.name.present? # explicit early return
|
|
4949
|
+
error! "name is required" # raise to reject the save
|
|
4950
|
+
end
|
|
4951
|
+
```
|
|
4952
|
+
|
|
4953
|
+
`self` inside the block is the `Parse::Webhooks::Payload`, so `parse_object`, `params`, and `error!` are available directly. As anywhere in Ruby, `return` ends the handler immediately — to run work *after* the response is sent, use `after_response` (below) rather than code after the `return`.
|
|
4954
|
+
|
|
4955
|
+
#### Deferring work until after the response
|
|
4956
|
+
|
|
4957
|
+
`payload.after_response { … }` (alias `defer`) registers work to run **after** the webhook response has been sent, off the critical path of the save or function the client is waiting on. The handler returns its value synchronously; the deferred block runs afterward — ideal for search indexing, cache warming, or fan-out that should not add latency.
|
|
4958
|
+
|
|
4959
|
+
```ruby
|
|
4960
|
+
Parse::Webhooks.route :after_save, :Post do
|
|
4961
|
+
post = parse_object
|
|
4962
|
+
after_response { SearchIndex.reindex(post.id) } # runs after the reply is sent
|
|
4963
|
+
post
|
|
4964
|
+
end
|
|
4965
|
+
```
|
|
4966
|
+
|
|
4967
|
+
Under Puma or Unicorn the block runs via `rack.after_reply` once the response is flushed (same worker thread, zero added round-trip latency); on a server without it (e.g. WEBrick) it falls back to a detached thread. Multiple blocks run in order and are isolated — one raising affects neither the response nor the others. Notes: deferred blocks run **only on the success path** (a rejected `before_save` runs none), "after the response" is **not** "after the row commits" (don't rely on the persisted row inside the block), and the work is **in-process and best-effort** — it dies with the worker, so for anything that *must* happen use a durable job queue (Sidekiq / ActiveJob). Blocks are drained only when the payload runs through the mounted `Parse::Webhooks` Rack app (a no-op under direct `run_function` / `call_route`). See the [Cloud Code Webhooks Guide](docs/webhooks_guide.md#deferring-work-until-after-the-response).
|
|
4968
|
+
|
|
4858
4969
|
> **Your model's `after_save` callbacks run here too.** When an `after_save` /
|
|
4859
4970
|
> `after_create` trigger fires, the webhook rebuilds the `Parse::Object` from the
|
|
4860
4971
|
> payload and runs that model's ActiveModel `after_save` / `after_create`
|
|
@@ -4866,6 +4977,57 @@ For any `after_*` hook, return values are not needed since Parse does not utiliz
|
|
|
4866
4977
|
> for saves from other clients (JS / iOS / REST), the webhook runs them, since
|
|
4867
4978
|
> the SDK never had the chance.
|
|
4868
4979
|
|
|
4980
|
+
#### ActiveModel callbacks vs. Parse Server triggers
|
|
4981
|
+
|
|
4982
|
+
The SDK exposes the full ActiveModel lifecycle on every model
|
|
4983
|
+
(`before_validation`, `before_save`/`after_save`, `before_create`/`after_create`,
|
|
4984
|
+
`before_update`/`after_update`, `before_destroy`/`after_destroy`). Parse Server,
|
|
4985
|
+
separately, exposes a fixed set of **webhook trigger types**. They are not
|
|
4986
|
+
one-to-one — the SDK maps between them, and a webhook must be **registered** for
|
|
4987
|
+
your ActiveModel logic to run server-side for non-Ruby clients (JS / iOS / REST /
|
|
4988
|
+
Dashboard). Without a registered webhook, that logic runs only in the Ruby
|
|
4989
|
+
process that initiated the save.
|
|
4990
|
+
|
|
4991
|
+
Supported Parse Server trigger types: `beforeSave`/`afterSave`,
|
|
4992
|
+
`beforeDelete`/`afterDelete`, `beforeFind`/`afterFind`, `beforeLogin`/`afterLogin`,
|
|
4993
|
+
`afterLogout`, `beforePasswordResetRequest`, `beforeConnect`,
|
|
4994
|
+
`beforeSubscribe`/`afterEvent`, and file triggers on the `@File` pseudo-class.
|
|
4995
|
+
|
|
4996
|
+
The **authentication** triggers (`beforeLogin`/`afterLogin`/`afterLogout`/
|
|
4997
|
+
`beforePasswordResetRequest`) and **LiveQuery** triggers (`beforeConnect`/
|
|
4998
|
+
`beforeSubscribe`/`afterEvent`) route as first-class shapes — predicates
|
|
4999
|
+
(`before_login?` … `after_event?`, `auth_trigger?`/`live_query_trigger?`), an
|
|
5000
|
+
`event` accessor, and top-level `sessionToken` capture into `payload.session_token`.
|
|
5001
|
+
None of them run ActiveModel `save`/`create`/`destroy` callbacks, even though the
|
|
5002
|
+
auth triggers carry a `_User`/`_Session`. Parse Server **ignores the response body**
|
|
5003
|
+
for all of them, so the only signal that affects the operation is rejection, and
|
|
5004
|
+
only on the `before*` variants: returning `false` (or calling `error!`) from a
|
|
5005
|
+
`before_login`/`before_connect`/`before_subscribe`/`before_password_reset_request`
|
|
5006
|
+
handler denies the operation, while anything else is a success no-op. (LiveQuery
|
|
5007
|
+
triggers are delivered over HTTP only in a co-located single-process setup;
|
|
5008
|
+
`beforeConnect` is effectively in-process-only.)
|
|
5009
|
+
|
|
5010
|
+
Key relationship — **`beforeSave`/`afterSave` carry the create variants**. Parse
|
|
5011
|
+
Server has **no `beforeCreate`/`afterCreate` trigger** (it rejects them). The SDK
|
|
5012
|
+
runs your `before_create`/`after_create` callbacks *inside* the
|
|
5013
|
+
`beforeSave`/`afterSave` handler for new objects, in ActiveModel order
|
|
5014
|
+
(`before_save → before_create`, `after_create → after_save`). So **registering a
|
|
5015
|
+
`beforeSave` webhook enables both `before_save` and `before_create`**, and
|
|
5016
|
+
`afterSave` enables both `after_save` and `after_create`. Requesting a create
|
|
5017
|
+
webhook raises with guidance pointing you at the save trigger.
|
|
5018
|
+
|
|
5019
|
+
> **`after_save` is synchronous and on the critical path.** Parse Server waits
|
|
5020
|
+
> for the webhook to return before completing the client's write — even on
|
|
5021
|
+
> `afterSave`, whose return value is a no-op. Treat `after_save` as a place to
|
|
5022
|
+
> **enqueue** background work, not to run long logic inline, and avoid saving
|
|
5023
|
+
> other objects inside it (each cascading save fires more webhooks). `beforeSave`
|
|
5024
|
+
> can mutate or reject the write, so it is necessarily inline — keep it lean.
|
|
5025
|
+
|
|
5026
|
+
For the full picture — trigger types, registration, the synchronous-latency
|
|
5027
|
+
model, the Ruby-initiated dedup, and inbound replay/freshness protection — see
|
|
5028
|
+
the [Cloud Code Webhooks Guide](docs/webhooks_guide.md) and
|
|
5029
|
+
[`examples/webhook_server.rb`](examples/webhook_server.rb).
|
|
5030
|
+
|
|
4869
5031
|
#### Trigger object state
|
|
4870
5032
|
|
|
4871
5033
|
Because the trigger payload is server-authoritative, the `parse_object` your
|
|
@@ -4930,6 +5092,80 @@ double-firing. `:if`/`:unless` conditions on these callbacks are honored.
|
|
|
4930
5092
|
> most for client-initiated saves, where the callback runs inside the webhook —
|
|
4931
5093
|
> Ruby-SDK saves run it in-process after their own REST response instead.
|
|
4932
5094
|
|
|
5095
|
+
#### Acting as the calling user
|
|
5096
|
+
|
|
5097
|
+
Parse Server includes the caller's live session token (`user.sessionToken`) in
|
|
5098
|
+
every trigger webhook fired by a logged-in user (it is absent for a master-key
|
|
5099
|
+
request). A handler can opt in to acting on the server **as that user** —
|
|
5100
|
+
authorized by Parse Server with full ACL, CLP, and `protectedFields`
|
|
5101
|
+
enforcement — instead of reaching for the application master key. Three opt-in
|
|
5102
|
+
handles are available on the `payload`:
|
|
5103
|
+
|
|
5104
|
+
| Handle | Returns | `nil` when |
|
|
5105
|
+
|---|---|---|
|
|
5106
|
+
| `payload.session_token` | the caller's raw token (`String`) | master-key request (no user) |
|
|
5107
|
+
| `payload.user_agent(**opts)` | a non-master `Parse::Agent` in **client mode**, token bound | no token |
|
|
5108
|
+
| `payload.user_client` | a non-master `Parse::Client` with the token **bound** | no token |
|
|
5109
|
+
|
|
5110
|
+
```ruby
|
|
5111
|
+
Parse::Webhooks.route :after_save, :Post do
|
|
5112
|
+
next true unless session_token? # master-key save → no caller token
|
|
5113
|
+
|
|
5114
|
+
# A client-mode Parse::Agent scoped to the caller (read tools only unless
|
|
5115
|
+
# you pass allow_mutations: true). ACL/CLP enforced; no master-key fallback.
|
|
5116
|
+
visible = user_agent.execute(:query_class, class_name: "Post", limit: 20)
|
|
5117
|
+
|
|
5118
|
+
# …or a raw user-scoped Parse::Client. The token is BOUND, so plain REST
|
|
5119
|
+
# calls authorize as the user with no per-call session_token: argument:
|
|
5120
|
+
mine = user_client.request(:get, "classes/Post").result
|
|
5121
|
+
true
|
|
5122
|
+
end
|
|
5123
|
+
```
|
|
5124
|
+
|
|
5125
|
+
The token is captured before the payload's credential scrubbing runs, so it is
|
|
5126
|
+
still removed from `payload.user` / `payload.object` and never appears in
|
|
5127
|
+
`payload.as_json`, `payload.inspect`, or the request log. `user_client` binds it
|
|
5128
|
+
via the `Parse::Client.new(session_token:)` option, applied as the
|
|
5129
|
+
lowest-priority auth fallback — an explicit per-call `session_token:`, a
|
|
5130
|
+
`Parse.with_session` block, or an explicit `use_master_key: true` all still take
|
|
5131
|
+
precedence. This applies to **webhook-delivered** triggers (including the model
|
|
5132
|
+
`webhook :before_save` DSL, which is HTTP-delivered); a genuine in-process
|
|
5133
|
+
ActiveModel `before_save :method` callback has no incoming request and therefore
|
|
5134
|
+
no caller token.
|
|
5135
|
+
|
|
5136
|
+
The same user-scoped client is available on the **client side**. Two shapes:
|
|
5137
|
+
|
|
5138
|
+
```ruby
|
|
5139
|
+
# 1. A client object you can pass around (carries the user's token + the
|
|
5140
|
+
# server connection, no master key):
|
|
5141
|
+
client = Parse::User.login(username, password).session_client
|
|
5142
|
+
Parse::Query.new("Post", client: client).results # runs as the user
|
|
5143
|
+
# (Parse.client.become(token) builds the same thing from any token.)
|
|
5144
|
+
|
|
5145
|
+
# 2. A block that runs ordinary model operations as the user, without
|
|
5146
|
+
# threading session_token: through each call:
|
|
5147
|
+
Parse::User.login(username, password).with_session do
|
|
5148
|
+
Post.query.count # counts only the user's readable Posts
|
|
5149
|
+
Post.create(title: "Hi") # created under the user's permissions
|
|
5150
|
+
end
|
|
5151
|
+
```
|
|
5152
|
+
|
|
5153
|
+
`with_session` (on a `Parse::User`, a `Parse::Client`, or `Parse.with_session`)
|
|
5154
|
+
scopes by binding the token as the ambient session — it authorizes
|
|
5155
|
+
**REST-routed** operations (`find` / `get` / `count` / `save`) as the user. It
|
|
5156
|
+
does **not** scope mongo-direct queries (`results_direct`, `aggregate`, Atlas
|
|
5157
|
+
search): those resolve auth from the query's own `session_token:` / `acl_user:`
|
|
5158
|
+
and otherwise run in **master** mode, so scope them explicitly with a per-query
|
|
5159
|
+
`session_token:` or a scoped `Parse::Agent`. (To run a query as a user *without*
|
|
5160
|
+
a token — via the master key and SDK-side ACL simulation — use
|
|
5161
|
+
`Parse::Query#scope_to_user(user)`.) `Parse::Client#anonymous` builds the same
|
|
5162
|
+
non-master client with no token, for an explicitly unauthenticated request.
|
|
5163
|
+
|
|
5164
|
+
To explore a server as a specific user from this repo, `rake client:console`
|
|
5165
|
+
opens an IRB session whose default client is a non-master client bound to a user
|
|
5166
|
+
(session token, login, or anonymous), so every query in the session runs with
|
|
5167
|
+
that user's ACL/CLP — with `whoami` and `as_master { … }` helpers.
|
|
5168
|
+
|
|
4933
5169
|
`before_save` and `before_delete` hooks have special functionality and multiple ways to halt operations:
|
|
4934
5170
|
|
|
4935
5171
|
1. **Using `error!` method**: Calling `error!` will return an error response to Parse Server
|
|
@@ -5628,6 +5864,13 @@ The integration tests use Docker Compose to spin up a Parse Server instance with
|
|
|
5628
5864
|
- Docker and Docker Compose installed
|
|
5629
5865
|
- Ruby environment with bundler
|
|
5630
5866
|
|
|
5867
|
+
> **Always run the suite with `bundle exec`.** Newer `minitest` (6.0+) moved
|
|
5868
|
+
> `minitest/mock` out into a separate gem, so a bare `ruby`/`rake` invocation
|
|
5869
|
+
> activates minitest 6 and then fails to load `minitest/mock`, aborting every
|
|
5870
|
+
> test at load time with `cannot load such file -- minitest/mock (LoadError)`.
|
|
5871
|
+
> Running through bundler pins the locked versions and avoids this. If you hit
|
|
5872
|
+
> that LoadError, prefix the command with `bundle exec`.
|
|
5873
|
+
|
|
5631
5874
|
#### Setup and Running Tests
|
|
5632
5875
|
|
|
5633
5876
|
1. **Enable Docker Tests**: Set the environment variable to enable Docker-based tests:
|
|
@@ -5712,6 +5955,56 @@ docker compose -f scripts/docker/docker-compose.test.yml up -d
|
|
|
5712
5955
|
curl -s http://localhost:29337/parse/health # -> {"status":"ok"}
|
|
5713
5956
|
```
|
|
5714
5957
|
|
|
5958
|
+
#### Network Exposure and the Preflight Guard
|
|
5959
|
+
|
|
5960
|
+
Every service binds to loopback (`127.0.0.1`) by default, and the default
|
|
5961
|
+
credentials above are committed to this repository — safe in combination,
|
|
5962
|
+
since nothing off the host can reach them. Each bind is overridable
|
|
5963
|
+
(`PARSE_BIND`, `MONGO_BIND`, `REDIS_BIND`, `DASHBOARD_BIND`) for the
|
|
5964
|
+
occasional need to attach a remote client while debugging.
|
|
5965
|
+
|
|
5966
|
+
That override is a footgun: pointing a bind at `0.0.0.0` while the default
|
|
5967
|
+
credentials are still in force would publish an admin-credentialed stack
|
|
5968
|
+
(Mongo `admin:password`, master key `psnextItMasterKey`) onto your LAN. A
|
|
5969
|
+
`preflight` service runs before anything else and **refuses to start the
|
|
5970
|
+
stack** in exactly that case. To proceed, do one of:
|
|
5971
|
+
|
|
5972
|
+
```bash
|
|
5973
|
+
# 1. Keep it loopback (the default) — just omit the *_BIND override.
|
|
5974
|
+
|
|
5975
|
+
# 2. Supply real credentials instead of the committed test defaults.
|
|
5976
|
+
PARSE_MASTER_KEY="$(openssl rand -hex 24)" \
|
|
5977
|
+
MONGO_ROOT_PASSWORD="$(openssl rand -hex 24)" \
|
|
5978
|
+
MONGO_BIND=0.0.0.0 \
|
|
5979
|
+
docker compose -f scripts/docker/docker-compose.test.yml up -d
|
|
5980
|
+
|
|
5981
|
+
# 3. Acknowledge the exposure on a trusted, isolated network.
|
|
5982
|
+
ALLOW_INSECURE_BIND=1 MONGO_BIND=0.0.0.0 \
|
|
5983
|
+
docker compose -f scripts/docker/docker-compose.test.yml up -d
|
|
5984
|
+
```
|
|
5985
|
+
|
|
5986
|
+
#### Secret Injection (real credentials)
|
|
5987
|
+
|
|
5988
|
+
The committed defaults are deliberately non-secret, so the loopback stack
|
|
5989
|
+
needs no secrets manager. If you point the stack at *real or shared*
|
|
5990
|
+
credentials (option 2 above, or a staging Mongo), keep them out of your
|
|
5991
|
+
shell history and the compose file by injecting them at launch. The stack
|
|
5992
|
+
reads plain environment variables, so any injector works:
|
|
5993
|
+
|
|
5994
|
+
```bash
|
|
5995
|
+
# 1Password CLI — secrets resolved from an op:// .env reference file.
|
|
5996
|
+
op run --env-file=.env.secrets -- \
|
|
5997
|
+
docker compose -f scripts/docker/docker-compose.test.yml up -d
|
|
5998
|
+
|
|
5999
|
+
# Doppler — secrets pulled from a configured project/config.
|
|
6000
|
+
doppler run -- \
|
|
6001
|
+
docker compose -f scripts/docker/docker-compose.test.yml up -d
|
|
6002
|
+
```
|
|
6003
|
+
|
|
6004
|
+
Use the committed `.env.sample` as the reference for which variables each
|
|
6005
|
+
side expects; copy it to a gitignored `.env` (or an `op://`-referenced
|
|
6006
|
+
`.env.secrets`) and fill in real values there.
|
|
6007
|
+
|
|
5715
6008
|
#### Environment Variables
|
|
5716
6009
|
|
|
5717
6010
|
The defaults above are baked into the Compose file and the test helpers, so the
|