parse-stack-next 5.2.0 → 5.3.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/CHANGELOG.md +240 -0
- data/Gemfile.lock +1 -1
- data/README.md +195 -1
- data/Rakefile +193 -40
- data/docs/client_sdk_guide.md +33 -0
- data/docs/mcp_guide.md +135 -4
- data/lib/parse/client.rb +119 -7
- data/lib/parse/model/associations/belongs_to.rb +47 -0
- data/lib/parse/model/classes/user.rb +20 -0
- data/lib/parse/model/core/actions.rb +7 -9
- 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 +70 -0
- data/lib/parse/model/file.rb +35 -2
- data/lib/parse/model/object.rb +41 -0
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +156 -1
- data/lib/parse/webhooks/payload.rb +205 -34
- data/lib/parse/webhooks.rb +15 -3
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 90a966c230e0a0e2e6fd814170726e3af713dae06c0341a00cd7581eecbe53ee
|
|
4
|
+
data.tar.gz: b171ed432c7ea7806ec07653b47122bd6102a16046c980d915fe45f98ffbca32
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 510f3f2deb860cedc73f11455fb5d4789c327923abebad4e26b223ae9c385374ffa23bc9564561e7d4dddb31b0c1a015b7e7222dacd7c0b18bdb01199580d7a0
|
|
7
|
+
data.tar.gz: cd0fb4c706b3c33f12059c7431cf75abd792cbd5ba8b193dd43256aaa70c8fcdd3f86e10b3ad8ffb25c8e9ccc3f985a61bcaa8b49d24badf6f8358cec2b16f3d
|
data/.bundle/config
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,245 @@
|
|
|
1
1
|
## parse-stack-next Changelog
|
|
2
2
|
|
|
3
|
+
### 5.3.0
|
|
4
|
+
|
|
5
|
+
#### Run webhook handlers as the calling user
|
|
6
|
+
|
|
7
|
+
Parse Server includes the caller's live session token (`user.sessionToken`) in
|
|
8
|
+
every trigger webhook fired by a logged-in user. A handler can now opt in to
|
|
9
|
+
acting on the server *as that user* — with full ACL, CLP, and `protectedFields`
|
|
10
|
+
enforcement — instead of reaching for the application master key.
|
|
11
|
+
|
|
12
|
+
- **NEW**: `Parse::Webhooks::Payload#session_token` exposes the caller's session
|
|
13
|
+
token, captured from the incoming payload. It is `nil` for a master-key
|
|
14
|
+
request (which carries no user). The token is captured before the payload's
|
|
15
|
+
credential scrubbing runs, so it is still removed from `payload.user` /
|
|
16
|
+
`payload.object` and never appears in `payload.as_json` or the request log.
|
|
17
|
+
- **NEW**: `Parse::Webhooks::Payload#user_agent` returns a non-master
|
|
18
|
+
`Parse::Agent` bound to the caller's session token, so it runs in client mode
|
|
19
|
+
and every query is authorized as the user. Returns `nil` when there is no
|
|
20
|
+
token.
|
|
21
|
+
- **NEW**: `Parse::Webhooks::Payload#user_client` returns a non-master
|
|
22
|
+
`Parse::Client` that binds the caller's session token, so even raw REST calls
|
|
23
|
+
through it are authorized as the user with no further ceremony. Returns `nil`
|
|
24
|
+
when there is no token.
|
|
25
|
+
- **NEW**: `Parse::Client.new` accepts a `session_token:` option that binds a
|
|
26
|
+
token to the client. It is applied as the lowest-priority auth fallback on
|
|
27
|
+
every request (explicit per-call `session_token:` and `Parse.with_session`
|
|
28
|
+
still take precedence, and an explicit `use_master_key: true` skips it).
|
|
29
|
+
Pair it with `master_key: nil` to build a user-scoped client.
|
|
30
|
+
- **NEW**: `Parse::Client#become(session_token)` returns a new non-master client
|
|
31
|
+
that mirrors the receiver's connection settings (`server_url`/`app_id`/
|
|
32
|
+
`api_key`) but binds the given token — the primitive behind `payload.user_client`
|
|
33
|
+
and `Parse::User#session_client`. `Parse::Client#anonymous` returns the same
|
|
34
|
+
shape with no token (unauthenticated REST) to drop a bound identity.
|
|
35
|
+
- **NEW**: `Parse::User#session_client` returns a non-master `Parse::Client`
|
|
36
|
+
bound to a logged-in user's session token — the client-side counterpart of
|
|
37
|
+
`payload.user_client` (e.g. `Parse::User.login(u, p).session_client`).
|
|
38
|
+
- **NEW**: `Parse::Client#with_session { ... }` runs a block with the client's
|
|
39
|
+
bound session token active as the ambient session, so REST-routed operations
|
|
40
|
+
(`find`/`get`/`count`/`save`) inside it are authorized as the user without
|
|
41
|
+
passing `session_token:` per call (the client-receiver flavor of
|
|
42
|
+
`Parse.with_session` / `Parse::User#with_session`). Mongo-direct queries
|
|
43
|
+
(`results_direct`, `aggregate`, Atlas search) are unaffected and still require
|
|
44
|
+
explicit per-query scoping.
|
|
45
|
+
- **NEW**: `rake client:console` opens an interactive console whose default
|
|
46
|
+
client is a non-master client bound to a user (session token, login, or
|
|
47
|
+
anonymous), so every query in the session runs with that user's ACL / CLP /
|
|
48
|
+
`protectedFields` rather than the master key.
|
|
49
|
+
- **FIXED**: `Parse::Client#inspect` and `Parse::Webhooks::Payload#inspect` no
|
|
50
|
+
longer print the master key or session token (and, for the payload, the
|
|
51
|
+
pre-scrub raw credentials) in cleartext — they are redacted, so an error
|
|
52
|
+
reporter or stray `inspect` cannot leak them.
|
|
53
|
+
- **FIXED**: a whitespace-only `Parse::Client` `session_token:` is now treated
|
|
54
|
+
as no token, so it can never silently fall through to the master key.
|
|
55
|
+
|
|
56
|
+
#### Pluralized class-name aliases
|
|
57
|
+
|
|
58
|
+
Referencing the plural form of a model constant now resolves to that class, so
|
|
59
|
+
the plural reads naturally as a query entry point — `Posts.where(...).count`
|
|
60
|
+
works for a class `Post`.
|
|
61
|
+
|
|
62
|
+
- **NEW**: The plural alias is created lazily on first reference and is the
|
|
63
|
+
*same class object* as the singular (`Posts.equal?(Post)` is `true`), so every
|
|
64
|
+
class method works through it (`query`/`where`, `count`, `find`, `all`, and any
|
|
65
|
+
`scope`) and `Posts.parse_class` still returns `"Post"`. Because it is the same
|
|
66
|
+
class, it adds no `Parse::Object.descendants` entry and never registers a
|
|
67
|
+
separate Parse schema class.
|
|
68
|
+
- **NEW**: `pluralized_alias!` class macro for explicit opt-in — use it for a
|
|
69
|
+
custom plural, a class whose name ends in `s`, or a namespaced model (the alias
|
|
70
|
+
is defined on the enclosing module). It ignores the global flag and raises
|
|
71
|
+
`ArgumentError` rather than clobbering an existing unrelated constant.
|
|
72
|
+
- **NEW**: `Parse.pluralized_aliases` configuration (default `true`; opt out with
|
|
73
|
+
`Parse.pluralized_aliases = false` or `PARSE_PLURALIZED_ALIASES=false`).
|
|
74
|
+
- **CHANGED**: The automatic path is conservative — a class whose name already
|
|
75
|
+
ends in `s` is skipped, and a plural that does not singularize to a known
|
|
76
|
+
`Parse::Object` subclass falls through to a normal `NameError`.
|
|
77
|
+
|
|
78
|
+
#### Pointer associations declared with `property … as:` — BREAKING: now serialize as Pointers
|
|
79
|
+
|
|
80
|
+
Declaring a pointer with `property` instead of `belongs_to` used to fail
|
|
81
|
+
silently: the `as:` option was dropped and the field became a plain `:string`
|
|
82
|
+
column, so an assigned object was stored as a String and never serialized as a
|
|
83
|
+
Parse pointer. `property … as:` now resolves to the same pointer association as
|
|
84
|
+
`belongs_to`.
|
|
85
|
+
|
|
86
|
+
- **BREAKING**: a field declared with `property … as:` now serializes as a
|
|
87
|
+
Pointer (`{__type: "Pointer", …}`) instead of a String. If a deployed app
|
|
88
|
+
previously used `property … as:` and saved records, Parse Server pinned that
|
|
89
|
+
column as `String` on first write, and a Pointer write against it is rejected
|
|
90
|
+
with error 111 (schema type mismatch). Drop or migrate that column to a
|
|
91
|
+
Pointer type before deploying. Apps that never used `property … as:` are
|
|
92
|
+
unaffected — the option was a silent no-op, so no working pointer behavior
|
|
93
|
+
depended on it.
|
|
94
|
+
- **FIXED**: `property <name>, as: <class>` and `property <name>, :pointer` now
|
|
95
|
+
declare a pointer association identical to the equivalent `belongs_to` — same
|
|
96
|
+
`:pointer` field type, remote column, target-class reference, getter/setter,
|
|
97
|
+
dirty tracking, and `{__type: "Pointer", …}` wire form. Previously `as:` was
|
|
98
|
+
ignored and the field defaulted to `:string`, with no warning.
|
|
99
|
+
- **NEW**: `belongs_to` (and the delegating `property … as:`) raise an
|
|
100
|
+
`ArgumentError` at declaration time when `as:` names a scalar data type such
|
|
101
|
+
as `as: :string` — a scalar cannot be a pointer target. The message points to
|
|
102
|
+
the intended `property <name>, :<type>` form. The guard fires only on an
|
|
103
|
+
explicit `as:`, so existing `belongs_to :field` declarations are unaffected.
|
|
104
|
+
- **NEW**: `Parse.validate_associations!` verifies that every `belongs_to` /
|
|
105
|
+
`property … as:` pointer target and every `has_many` target — relation-,
|
|
106
|
+
array-, and query-backed — resolves to a known Parse class, reporting any
|
|
107
|
+
unresolved target as a `Class#field -> "Target"` offender. The query- and
|
|
108
|
+
array-backed `has_many` targets are the bucket where an `as:` typo otherwise
|
|
109
|
+
stays latent until the association is first traversed. Forward references are
|
|
110
|
+
legal at
|
|
111
|
+
declaration time and indistinguishable from typos there, so this cross-class
|
|
112
|
+
check is meant to run once after all models load (boot, CI, or a rake task).
|
|
113
|
+
- **IMPROVED**: `belongs_to` now stores `_description:` and `_enum:` agent
|
|
114
|
+
metadata, which previously only `property` retained. A documented pointer
|
|
115
|
+
association keeps its semantic description, including when declared through
|
|
116
|
+
`property … as:`.
|
|
117
|
+
|
|
118
|
+
#### afterSave create reports changed fields; file equality is force_ssl-consistent
|
|
119
|
+
|
|
120
|
+
A trigger handler that keys off dirty tracking (`*_changed?` / `changes`) now
|
|
121
|
+
sees every field an object was created with on an `afterSave` create, symmetric
|
|
122
|
+
with the existing `afterSave` update behavior, and two `Parse::File` values that
|
|
123
|
+
point at the same location compare equal regardless of `force_ssl`.
|
|
124
|
+
|
|
125
|
+
- **NEW**: On an `afterSave` trigger for a newly created object (no prior
|
|
126
|
+
persisted state), the built `Parse::Object` now marks every populated data
|
|
127
|
+
property as changed, so a handler can use `*_changed?` / `changes` / `changed`
|
|
128
|
+
uniformly across create and update. Previously the create object
|
|
129
|
+
was pristine, so `*_changed?` returned `false` for every field and a handler
|
|
130
|
+
that builds a payload from changed fields would see nothing on creates. A
|
|
131
|
+
property whose create value equals its declared `default:` (e.g.
|
|
132
|
+
`status: "draft"`, `count: 0`, `archived: false`) is correctly reported as
|
|
133
|
+
changed. System fields (`createdAt` / `updatedAt` / `ACL` / `objectId`) stay
|
|
134
|
+
clean — they are not reported as changed — and object readability, `new?`, and
|
|
135
|
+
`existed?` are unchanged. Credential and row-permission fields remain filtered
|
|
136
|
+
from the built object. The `afterSave` update path is unchanged.
|
|
137
|
+
- **CHANGED**: As a result of the above, model `after_save` / `after_create`
|
|
138
|
+
callbacks fired through the webhook dispatcher now observe an `afterSave`-create
|
|
139
|
+
object whose data fields are dirty (rather than pristine). A handler that
|
|
140
|
+
branched on dirty state for a created object should review that expectation;
|
|
141
|
+
the values themselves are unchanged.
|
|
142
|
+
- **FIXED**: `Parse::File#==` now compares both files through the canonical
|
|
143
|
+
`url` reader rather than the raw stored URL on one side, so the comparison is
|
|
144
|
+
symmetric (`a == b` matches `b == a`) and consistent when
|
|
145
|
+
`Parse::File.force_ssl` coerces a stored `http://` URL to `https://`. Two
|
|
146
|
+
files at the same location previously read as unequal under `force_ssl`, which
|
|
147
|
+
could spuriously mark a file property as changed during dirty tracking.
|
|
148
|
+
Signed-URL query parameters are still stripped before comparison, so a
|
|
149
|
+
re-signed URL for the same object compares equal while a different underlying
|
|
150
|
+
location does not.
|
|
151
|
+
- **NEW**: `Parse::File#content_signature` is the overridable seam that `==`
|
|
152
|
+
uses to decide whether two files refer to the same underlying file. It returns
|
|
153
|
+
the canonical `url` today (Parse Server's S3 files adapter exposes no content
|
|
154
|
+
digest); a files-adapter shim or `Parse::File` subclass can override it to key
|
|
155
|
+
equality off a content hash (S3 ETag / sha256) in the future without touching
|
|
156
|
+
dirty tracking.
|
|
157
|
+
|
|
158
|
+
### 5.2.1
|
|
159
|
+
|
|
160
|
+
#### Webhook trigger handlers now receive the full Parse object
|
|
161
|
+
|
|
162
|
+
Webhook trigger payloads (`beforeSave`/`afterSave`/`beforeDelete`/`afterDelete`)
|
|
163
|
+
are delivered by Parse Server and authenticated by the webhook key, so they are
|
|
164
|
+
trusted, server-authoritative state. Previously the payload was filtered through
|
|
165
|
+
the wide mass-assignment denylist, which stripped server-issued
|
|
166
|
+
`createdAt`/`updatedAt` (and other non-credential fields) before the handler
|
|
167
|
+
could see them. That broke `Parse::Object#existed?` and `#new?` inside
|
|
168
|
+
`afterSave` handlers — `existed?` always returned `false` and `new?` always
|
|
169
|
+
returned `true`, regardless of whether the object was created or updated — and
|
|
170
|
+
hid the object's timestamps and ACL from handler code.
|
|
171
|
+
|
|
172
|
+
- **FIXED**: `afterSave`/`beforeSave` handlers now receive the full object as
|
|
173
|
+
Parse Server sends it (`createdAt`, `updatedAt`, `ACL`, internal fields).
|
|
174
|
+
`Parse::Object#existed?` and `#new?` are now reliable inside `afterSave`
|
|
175
|
+
handlers. (Genuine credentials — session tokens and password hashes — are
|
|
176
|
+
still stripped, and `Parse::User` continues to protect `authData` on
|
|
177
|
+
`payload.user`.)
|
|
178
|
+
- **NEW**: `afterSave` handlers on an updated object now carry dirty tracking
|
|
179
|
+
relative to the prior state, so `title_changed?`, `changed`, and `changes`
|
|
180
|
+
work inside `afterSave` the same way they already did inside `beforeSave`.
|
|
181
|
+
- **CHANGED**: Inbound webhook trigger payloads are now scrubbed of genuine
|
|
182
|
+
credential material only (`sessionToken`, `_hashed_password`,
|
|
183
|
+
`_password_history`) rather than the full mass-assignment denylist. Protection
|
|
184
|
+
against persisting forged privileged fields remains on the write path: a save
|
|
185
|
+
emits only declared, dirty-tracked properties, and an after-trigger response
|
|
186
|
+
is `true`/`false`, so forged `_rperm`/`_wperm`/`authData` cannot be persisted
|
|
187
|
+
through a handler. This applies only to the inbound webhook trigger payload;
|
|
188
|
+
client login/signup responses are unaffected and still return session tokens.
|
|
189
|
+
- **CHANGED**: In an `afterSave` handler, `new?` now correctly returns `false`
|
|
190
|
+
(the object is already persisted) where the previous timestamp-stripping bug
|
|
191
|
+
made it return `true`. Use `existed?` to distinguish create from update inside
|
|
192
|
+
`afterSave` (`existed? == false` for a create, `true` for an update); `new?`
|
|
193
|
+
is intended for `beforeSave`.
|
|
194
|
+
- **CHANGED**: Dirty-gated `after_save` side effects now fire on client/REST-
|
|
195
|
+
initiated saves where they previously silently no-op'd. With timestamps and
|
|
196
|
+
dirty tracking restored, a callback such as `after_save { notify if
|
|
197
|
+
title_changed? }` will now activate for objects created or updated via REST /
|
|
198
|
+
JS cloud code, not only for Ruby-model saves.
|
|
199
|
+
|
|
200
|
+
```ruby
|
|
201
|
+
Parse::Webhooks.route :after_save, "Post" do
|
|
202
|
+
post = parse_object
|
|
203
|
+
|
|
204
|
+
if post.existed? # now reliable: false on create, true on update
|
|
205
|
+
NotificationService.changed(post) if post.title_changed?
|
|
206
|
+
else
|
|
207
|
+
post.create_default_associations!
|
|
208
|
+
end
|
|
209
|
+
true
|
|
210
|
+
end
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
#### Lifecycle callbacks run in ActiveModel order for client-initiated saves
|
|
214
|
+
|
|
215
|
+
Parse Server exposes no separate `beforeCreate`/`afterCreate` triggers — only
|
|
216
|
+
`beforeSave` and `afterSave`. The webhook layer now runs the model lifecycle
|
|
217
|
+
callbacks for a client-initiated create in the canonical ActiveModel order:
|
|
218
|
+
`before_save` → `before_create` (in the `beforeSave` webhook) then
|
|
219
|
+
`after_create` → `after_save` (in the `afterSave` webhook).
|
|
220
|
+
|
|
221
|
+
- **FIXED**: `before_create` callbacks now run for client/REST/JS/Auth0-created
|
|
222
|
+
objects. The `beforeSave` webhook runs `before_create` after `before_save` for
|
|
223
|
+
new objects (an object with no `original`); previously `before_create` never
|
|
224
|
+
fired for non-Ruby creates, so create-time setup written as `before_create`
|
|
225
|
+
was silently skipped.
|
|
226
|
+
- **FIXED**: `after_save` no longer double-fires on client-initiated saves. The
|
|
227
|
+
`beforeSave` webhook entry point previously ran the full save callback chain,
|
|
228
|
+
firing `after_save` during `beforeSave` in addition to the `afterSave`
|
|
229
|
+
webhook. It now runs the before phase only.
|
|
230
|
+
- **NEW**: `Parse::Object#run_before_save_callbacks` and
|
|
231
|
+
`#run_before_create_callbacks` — the before-phase counterparts to the existing
|
|
232
|
+
`run_after_save_callbacks` / `run_after_create_callbacks`.
|
|
233
|
+
- **CHANGED**: `Parse::Object#prepare_save!` is retained as a back-compat alias
|
|
234
|
+
for `run_before_save_callbacks` and now runs the before phase only (it no
|
|
235
|
+
longer also fires `after_save`). The before-phase runners honor `:if`/`:unless`
|
|
236
|
+
callback conditions and the callback terminator.
|
|
237
|
+
- **NOTE**: the webhook layer runs `before_save`/`before_create` and
|
|
238
|
+
`after_create`/`after_save`, but not `before_update`/`after_update` — those
|
|
239
|
+
`:update`-specific callbacks fire only on Ruby-model saves, not for
|
|
240
|
+
client-initiated (REST/JS/Auth0) saves. Use `before_save`/`after_save` (which
|
|
241
|
+
run for every save) and branch on `existed?` if you need update-only logic.
|
|
242
|
+
|
|
3
243
|
### 5.2.0
|
|
4
244
|
|
|
5
245
|
#### Retrieval layer — `Parse::Retrieval` (`Parse::RAG`)
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -4,8 +4,15 @@
|
|
|
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.3
|
|
8
|
+
|
|
9
|
+
- **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)
|
|
10
|
+
- **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)
|
|
11
|
+
- **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)
|
|
12
|
+
|
|
7
13
|
### What's new in 5.2
|
|
8
14
|
|
|
15
|
+
- **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)
|
|
9
16
|
- **Retrieval layer — `Parse::Retrieval` (`Parse::RAG`)** — `Parse::Retrieval.retrieve(query:, klass:, k:, filter:, tenant_scope:, …)` embeds a natural-language query, runs Atlas `$vectorSearch` through the existing ACL-enforcing `find_similar`, and splits each retrieved document's text field into scored `Parse::Retrieval::Chunk`s. Chunking is presentation-only (embedding stays one-vector-per-record), via `Parse::Retrieval::Chunker::FixedSizeOverlap(size:, overlap:, by:, max_chunks_per_document:)` (subclass `Chunker::Base` for custom strategies). ACL is mongo-direct (no REST two-stage); tenant scope folds into the Atlas pre-filter
|
|
10
17
|
- **`semantic_search` agent tool + `agent_searchable`** — declare `agent_searchable field:, filter_fields:` on a model to expose it to the readonly, client-safe `semantic_search` tool. The handler enforces the full agent envelope: searchable-class allowlist, recursive underscore-key refusal + filter-field allowlist on input, `field_allowlist` projection plus tenant-scope re-assertion on output, and score quantization in non-admin contexts
|
|
11
18
|
- **MCP elicitation — human-in-the-loop approval** — opt in with `Parse::Agent.require_approval_for = [:write, :admin]` to require spec-native `elicitation/create` approval before destructive tool calls. A pluggable `agent.approval_gate` (reachable on the non-MCP path too) shows the dry-run diff and blocks on the client's reply; `call_method` resolves the *effective* tier from the target `agent_method`. Fails closed (no capability / no listening stream / non-streaming transport / timeout → refuse); replies are session-bound
|
|
@@ -295,6 +302,7 @@ The 1.x line is the original [`modernistik/parse-stack`](https://github.com/mode
|
|
|
295
302
|
- [Linking and Unlinking](#linking-and-unlinking)
|
|
296
303
|
- [Request Password Reset](#request-password-reset)
|
|
297
304
|
- [Modeling and Subclassing](#modeling-and-subclassing)
|
|
305
|
+
- [Pluralized class-name aliases](#pluralized-class-name-aliases)
|
|
298
306
|
- [Defining Properties](#defining-properties)
|
|
299
307
|
- [Accessor Aliasing](#accessor-aliasing)
|
|
300
308
|
- [Property Options](#property-options)
|
|
@@ -397,6 +405,7 @@ The 1.x line is the original [`modernistik/parse-stack`](https://github.com/mode
|
|
|
397
405
|
- [Cloud Code Webhooks](#cloud-code-webhooks)
|
|
398
406
|
- [Cloud Code Functions](#cloud-code-functions)
|
|
399
407
|
- [Cloud Code Triggers](#cloud-code-triggers)
|
|
408
|
+
- [Trigger object state](#trigger-object-state)
|
|
400
409
|
- [Mounting Webhooks Application](#mounting-webhooks-application)
|
|
401
410
|
- [Register Webhooks](#register-webhooks)
|
|
402
411
|
- [Parse REST API Client](#parse-rest-api-client)
|
|
@@ -1620,6 +1629,61 @@ class Commentary < Parse::Object
|
|
|
1620
1629
|
end
|
|
1621
1630
|
```
|
|
1622
1631
|
|
|
1632
|
+
### Pluralized class-name aliases
|
|
1633
|
+
|
|
1634
|
+
As a convenience, the plural form of a model constant resolves to that class, so the plural reads naturally at a query call site:
|
|
1635
|
+
|
|
1636
|
+
```ruby
|
|
1637
|
+
class Post < Parse::Object
|
|
1638
|
+
property :title, :string
|
|
1639
|
+
belongs_to :author, as: :user
|
|
1640
|
+
end
|
|
1641
|
+
|
|
1642
|
+
# `Posts` resolves to `Post` on first reference:
|
|
1643
|
+
Posts.where(:author.eq => current_user).count
|
|
1644
|
+
Posts.query(:title.exists => true).results
|
|
1645
|
+
Posts.find("abc123")
|
|
1646
|
+
```
|
|
1647
|
+
|
|
1648
|
+
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:
|
|
1649
|
+
|
|
1650
|
+
- Every class method works through it for free: `query`/`where`, `count`, `find`, `all`, and any `scope` you define.
|
|
1651
|
+
- `Posts.parse_class` still returns `"Post"`. The alias is purely a Ruby-constant convenience.
|
|
1652
|
+
- 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.
|
|
1653
|
+
|
|
1654
|
+
The automatic path is deliberately conservative:
|
|
1655
|
+
|
|
1656
|
+
- A class whose name already ends in `s` (for example `Status`, `Series`) is **skipped**, since its plural is ambiguous.
|
|
1657
|
+
- 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.
|
|
1658
|
+
|
|
1659
|
+
The feature is enabled by default. Opt out globally:
|
|
1660
|
+
|
|
1661
|
+
```ruby
|
|
1662
|
+
Parse.pluralized_aliases = false # programmatic
|
|
1663
|
+
# or
|
|
1664
|
+
# PARSE_PLURALIZED_ALIASES=false # environment
|
|
1665
|
+
```
|
|
1666
|
+
|
|
1667
|
+
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:
|
|
1668
|
+
|
|
1669
|
+
```ruby
|
|
1670
|
+
class Status < Parse::Object
|
|
1671
|
+
pluralized_alias! :Statuses # defines ::Statuses => Status
|
|
1672
|
+
end
|
|
1673
|
+
|
|
1674
|
+
module Blog
|
|
1675
|
+
class Article < Parse::Object
|
|
1676
|
+
pluralized_alias! # defines Blog::Articles => Blog::Article
|
|
1677
|
+
end
|
|
1678
|
+
end
|
|
1679
|
+
```
|
|
1680
|
+
|
|
1681
|
+
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).
|
|
1682
|
+
|
|
1683
|
+
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.
|
|
1684
|
+
|
|
1685
|
+
> **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`.
|
|
1686
|
+
|
|
1623
1687
|
### Defining Properties
|
|
1624
1688
|
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.
|
|
1625
1689
|
|
|
@@ -4825,7 +4889,9 @@ end
|
|
|
4825
4889
|
```
|
|
4826
4890
|
|
|
4827
4891
|
### Cloud Code Triggers
|
|
4828
|
-
You can register webhooks to handle the different object triggers: `:before_save`, `:after_save`, `:before_delete` and `:after_delete`. The `payload` object, which is an instance of `Parse::Webhooks::Payload`, contains several properties that represent the payload. One of the most important ones is `parse_object`, which will provide you with the instance of your specific Parse object.
|
|
4892
|
+
You can register webhooks to handle the different object triggers: `:before_save`, `:after_save`, `:before_delete` and `:after_delete`. The `payload` object, which is an instance of `Parse::Webhooks::Payload`, contains several properties that represent the payload. One of the most important ones is `parse_object`, which will provide you with the instance of your specific Parse object.
|
|
4893
|
+
|
|
4894
|
+
The `parse_object` handed to your handler is the **full object as Parse Server sent it** — `createdAt`/`updatedAt`, `ACL`, and internal fields all survive (only live credentials — session tokens and password hashes — are stripped; `Parse::User` additionally protects `authData` on `payload.user`). Both `:before_save` and `:after_save` objects carry **dirty tracking** of what changed (`name_changed?`, `changes`), and `Parse::Object#existed?` / `#new?` are reliable inside `:after_save`. See [Trigger object state](#trigger-object-state) below.
|
|
4829
4895
|
|
|
4830
4896
|
```ruby
|
|
4831
4897
|
# recommended way
|
|
@@ -4862,6 +4928,60 @@ For any `after_*` hook, return values are not needed since Parse does not utiliz
|
|
|
4862
4928
|
> for saves from other clients (JS / iOS / REST), the webhook runs them, since
|
|
4863
4929
|
> the SDK never had the chance.
|
|
4864
4930
|
|
|
4931
|
+
#### Trigger object state
|
|
4932
|
+
|
|
4933
|
+
Because the trigger payload is server-authoritative, the `parse_object` your
|
|
4934
|
+
handler receives is the complete object, and the usual `Parse::Object`
|
|
4935
|
+
introspection works inside the trigger:
|
|
4936
|
+
|
|
4937
|
+
| What you want to know | In `:before_save` | In `:after_save` |
|
|
4938
|
+
|---|---|---|
|
|
4939
|
+
| Is this a create or an update? | `parse_object.new?` (`true` = create) | `parse_object.existed?` (`false` = create) or `payload.original.nil?` |
|
|
4940
|
+
| What changed? | `name_changed?`, `changes`, `changed` | `name_changed?`, `changes`, `changed` (relative to the prior state) |
|
|
4941
|
+
| Server timestamps | not yet assigned (`new?` create) | `created_at` / `updated_at` populated |
|
|
4942
|
+
| The prior stored values | `payload.original_parse_object` | `payload.original_parse_object` |
|
|
4943
|
+
|
|
4944
|
+
Use `new?` in `:before_save` and `existed?` in `:after_save`. In `:after_save`
|
|
4945
|
+
the object is already persisted, so `new?` is `false` for both creates and
|
|
4946
|
+
updates — `existed?` (`created_at != updated_at`) is the create/update signal,
|
|
4947
|
+
equivalently `payload.original.nil?`.
|
|
4948
|
+
|
|
4949
|
+
```ruby
|
|
4950
|
+
Parse::Webhooks.route :after_save, :Post do
|
|
4951
|
+
post = parse_object
|
|
4952
|
+
if post.existed?
|
|
4953
|
+
Search.reindex(post) if post.title_changed? # update
|
|
4954
|
+
else
|
|
4955
|
+
post.create_default_associations! # first save
|
|
4956
|
+
end
|
|
4957
|
+
true
|
|
4958
|
+
end
|
|
4959
|
+
```
|
|
4960
|
+
|
|
4961
|
+
**Lifecycle callback order.** Parse Server has no separate `beforeCreate` /
|
|
4962
|
+
`afterCreate` triggers — only `beforeSave` and `afterSave`. The SDK runs your
|
|
4963
|
+
model's ActiveModel callbacks in canonical order across the two webhooks:
|
|
4964
|
+
|
|
4965
|
+
```
|
|
4966
|
+
beforeSave webhook : before_save → before_create (before_create only for new objects)
|
|
4967
|
+
[Parse Server persists]
|
|
4968
|
+
afterSave webhook : after_create → after_save (after_create only for new objects)
|
|
4969
|
+
```
|
|
4970
|
+
|
|
4971
|
+
So a model `before_create` / `after_create` callback runs for objects created by
|
|
4972
|
+
**any** client (REST / JS cloud code / Auth0 / iOS), not just Ruby-model saves —
|
|
4973
|
+
provided the corresponding trigger is registered with Parse Server (see
|
|
4974
|
+
[Register Webhooks](#register-webhooks)). These callbacks fire **once** per save;
|
|
4975
|
+
Ruby-SDK-initiated saves run them locally and the webhook skips them to avoid
|
|
4976
|
+
double-firing. `:if`/`:unless` conditions on these callbacks are honored.
|
|
4977
|
+
|
|
4978
|
+
> **`before_update` / `after_update` do not run from webhooks.** The webhook
|
|
4979
|
+
> layer runs `before_save` / `before_create` / `after_create` / `after_save`
|
|
4980
|
+
> only. The `:update`-specific callbacks fire on Ruby-model saves but **not**
|
|
4981
|
+
> for client-initiated (REST / JS / Auth0) saves, because Parse Server has no
|
|
4982
|
+
> `beforeUpdate` / `afterUpdate` trigger. For update-time logic that must run
|
|
4983
|
+
> for all clients, use `before_save` / `after_save` and branch on `existed?`.
|
|
4984
|
+
|
|
4865
4985
|
> **Keep `after_save` handlers fast.** Parse Server **waits** for the `after_save`
|
|
4866
4986
|
> webhook response before returning to the saving client (only LiveQuery events
|
|
4867
4987
|
> are truly fire-and-forget), so a slow handler adds latency to that client's
|
|
@@ -4872,6 +4992,80 @@ For any `after_*` hook, return values are not needed since Parse does not utiliz
|
|
|
4872
4992
|
> most for client-initiated saves, where the callback runs inside the webhook —
|
|
4873
4993
|
> Ruby-SDK saves run it in-process after their own REST response instead.
|
|
4874
4994
|
|
|
4995
|
+
#### Acting as the calling user
|
|
4996
|
+
|
|
4997
|
+
Parse Server includes the caller's live session token (`user.sessionToken`) in
|
|
4998
|
+
every trigger webhook fired by a logged-in user (it is absent for a master-key
|
|
4999
|
+
request). A handler can opt in to acting on the server **as that user** —
|
|
5000
|
+
authorized by Parse Server with full ACL, CLP, and `protectedFields`
|
|
5001
|
+
enforcement — instead of reaching for the application master key. Three opt-in
|
|
5002
|
+
handles are available on the `payload`:
|
|
5003
|
+
|
|
5004
|
+
| Handle | Returns | `nil` when |
|
|
5005
|
+
|---|---|---|
|
|
5006
|
+
| `payload.session_token` | the caller's raw token (`String`) | master-key request (no user) |
|
|
5007
|
+
| `payload.user_agent(**opts)` | a non-master `Parse::Agent` in **client mode**, token bound | no token |
|
|
5008
|
+
| `payload.user_client` | a non-master `Parse::Client` with the token **bound** | no token |
|
|
5009
|
+
|
|
5010
|
+
```ruby
|
|
5011
|
+
Parse::Webhooks.route :after_save, :Post do
|
|
5012
|
+
next true unless session_token? # master-key save → no caller token
|
|
5013
|
+
|
|
5014
|
+
# A client-mode Parse::Agent scoped to the caller (read tools only unless
|
|
5015
|
+
# you pass allow_mutations: true). ACL/CLP enforced; no master-key fallback.
|
|
5016
|
+
visible = user_agent.execute(:query_class, class_name: "Post", limit: 20)
|
|
5017
|
+
|
|
5018
|
+
# …or a raw user-scoped Parse::Client. The token is BOUND, so plain REST
|
|
5019
|
+
# calls authorize as the user with no per-call session_token: argument:
|
|
5020
|
+
mine = user_client.request(:get, "classes/Post").result
|
|
5021
|
+
true
|
|
5022
|
+
end
|
|
5023
|
+
```
|
|
5024
|
+
|
|
5025
|
+
The token is captured before the payload's credential scrubbing runs, so it is
|
|
5026
|
+
still removed from `payload.user` / `payload.object` and never appears in
|
|
5027
|
+
`payload.as_json`, `payload.inspect`, or the request log. `user_client` binds it
|
|
5028
|
+
via the `Parse::Client.new(session_token:)` option, applied as the
|
|
5029
|
+
lowest-priority auth fallback — an explicit per-call `session_token:`, a
|
|
5030
|
+
`Parse.with_session` block, or an explicit `use_master_key: true` all still take
|
|
5031
|
+
precedence. This applies to **webhook-delivered** triggers (including the model
|
|
5032
|
+
`webhook :before_save` DSL, which is HTTP-delivered); a genuine in-process
|
|
5033
|
+
ActiveModel `before_save :method` callback has no incoming request and therefore
|
|
5034
|
+
no caller token.
|
|
5035
|
+
|
|
5036
|
+
The same user-scoped client is available on the **client side**. Two shapes:
|
|
5037
|
+
|
|
5038
|
+
```ruby
|
|
5039
|
+
# 1. A client object you can pass around (carries the user's token + the
|
|
5040
|
+
# server connection, no master key):
|
|
5041
|
+
client = Parse::User.login(username, password).session_client
|
|
5042
|
+
Parse::Query.new("Post", client: client).results # runs as the user
|
|
5043
|
+
# (Parse.client.become(token) builds the same thing from any token.)
|
|
5044
|
+
|
|
5045
|
+
# 2. A block that runs ordinary model operations as the user, without
|
|
5046
|
+
# threading session_token: through each call:
|
|
5047
|
+
Parse::User.login(username, password).with_session do
|
|
5048
|
+
Post.query.count # counts only the user's readable Posts
|
|
5049
|
+
Post.create(title: "Hi") # created under the user's permissions
|
|
5050
|
+
end
|
|
5051
|
+
```
|
|
5052
|
+
|
|
5053
|
+
`with_session` (on a `Parse::User`, a `Parse::Client`, or `Parse.with_session`)
|
|
5054
|
+
scopes by binding the token as the ambient session — it authorizes
|
|
5055
|
+
**REST-routed** operations (`find` / `get` / `count` / `save`) as the user. It
|
|
5056
|
+
does **not** scope mongo-direct queries (`results_direct`, `aggregate`, Atlas
|
|
5057
|
+
search): those resolve auth from the query's own `session_token:` / `acl_user:`
|
|
5058
|
+
and otherwise run in **master** mode, so scope them explicitly with a per-query
|
|
5059
|
+
`session_token:` or a scoped `Parse::Agent`. (To run a query as a user *without*
|
|
5060
|
+
a token — via the master key and SDK-side ACL simulation — use
|
|
5061
|
+
`Parse::Query#scope_to_user(user)`.) `Parse::Client#anonymous` builds the same
|
|
5062
|
+
non-master client with no token, for an explicitly unauthenticated request.
|
|
5063
|
+
|
|
5064
|
+
To explore a server as a specific user from this repo, `rake client:console`
|
|
5065
|
+
opens an IRB session whose default client is a non-master client bound to a user
|
|
5066
|
+
(session token, login, or anonymous), so every query in the session runs with
|
|
5067
|
+
that user's ACL/CLP — with `whoami` and `as_master { … }` helpers.
|
|
5068
|
+
|
|
4875
5069
|
`before_save` and `before_delete` hooks have special functionality and multiple ways to halt operations:
|
|
4876
5070
|
|
|
4877
5071
|
1. **Using `error!` method**: Calling `error!` will return an error response to Parse Server
|