parse-stack-next 5.0.1 → 5.1.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/.github/ISSUE_TEMPLATE/bug_report.yml +105 -0
- data/.github/ISSUE_TEMPLATE/feature_request.yml +67 -0
- data/.github/dependabot.yml +13 -0
- data/.github/workflows/codeql.yml +1 -1
- data/.github/workflows/docs.yml +3 -3
- data/.github/workflows/release.yml +14 -3
- data/.github/workflows/ruby.yml +1 -1
- data/.gitignore +1 -0
- data/.yardopts +19 -0
- data/CHANGELOG.md +792 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +8 -5
- data/README.md +15 -0
- data/Rakefile +5 -1
- data/docs/acl_clp_guide.md +553 -0
- data/docs/atlas_vector_search_guide.md +123 -22
- data/docs/client_sdk_guide.md +201 -5
- data/docs/usage_guide.md +21 -0
- data/docs/yard-template/default/fulldoc/html/css/common.css +1222 -0
- data/docs/yard-template/default/fulldoc/html/css/full_list.css +387 -0
- data/lib/parse/agent/tools.rb +153 -1
- data/lib/parse/cache/redis.rb +53 -0
- data/lib/parse/client/caching.rb +18 -1
- data/lib/parse/client.rb +79 -12
- data/lib/parse/embeddings/cohere.rb +143 -6
- data/lib/parse/embeddings/provider.rb +20 -2
- data/lib/parse/embeddings/voyage.rb +102 -0
- data/lib/parse/embeddings.rb +332 -1
- data/lib/parse/live_query/client.rb +167 -4
- data/lib/parse/live_query/configuration.rb +12 -0
- data/lib/parse/live_query/subscription.rb +55 -2
- data/lib/parse/live_query.rb +123 -1
- data/lib/parse/lock.rb +342 -0
- data/lib/parse/lock_backend.rb +308 -0
- data/lib/parse/model/classes/audience.rb +5 -0
- data/lib/parse/model/classes/installation.rb +122 -0
- data/lib/parse/model/classes/job_schedule.rb +3 -1
- data/lib/parse/model/classes/job_status.rb +4 -1
- data/lib/parse/model/classes/push_status.rb +4 -1
- data/lib/parse/model/classes/session.rb +7 -0
- data/lib/parse/model/classes/user.rb +204 -0
- data/lib/parse/model/core/create_lock.rb +28 -146
- data/lib/parse/model/core/embed_managed.rb +162 -13
- data/lib/parse/model/core/parse_reference.rb +17 -1
- data/lib/parse/model/core/querying.rb +26 -2
- data/lib/parse/model/file.rb +523 -18
- data/lib/parse/query.rb +31 -1
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +98 -1
- data/parse-stack-next.gemspec +2 -2
- metadata +17 -7
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
# Atlas Vector Search Guide
|
|
2
2
|
|
|
3
|
-
Parse Stack
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
logging behavior callers need to know about.
|
|
3
|
+
Parse Stack ships first-class support for MongoDB Atlas `$vectorSearch`
|
|
4
|
+
against Parse classes. This guide covers the full surface: declaring
|
|
5
|
+
`:vector` properties, registering embedding providers, running
|
|
6
|
+
`find_similar` queries, the `embed` and `embed_image` write-side
|
|
7
|
+
macros, Atlas index management, AS::N telemetry, and the constraint
|
|
8
|
+
and logging behavior callers need to know about.
|
|
9
|
+
|
|
10
|
+
v5.0 introduced the text-embedding path; v5.1 adds image embedding via
|
|
11
|
+
the new `embed_image` macro, `Voyage#embed_image`
|
|
12
|
+
(`voyage-multimodal-3`, 1024-dim), and `Cohere#embed_image`
|
|
13
|
+
(`embed-v4.0`, 1536-dim). Image inputs are URL-only in v5.1 (the SDK
|
|
14
|
+
forwards the file URL to the provider; the SDK does not fetch image
|
|
15
|
+
bytes) and are gated behind an explicit operator opt-in plus a CDN
|
|
16
|
+
allowlist — see §Image embedding below.
|
|
9
17
|
|
|
10
18
|
For the underlying mongo-direct enforcement model that vector search
|
|
11
19
|
inherits, see [mongodb_direct_guide.md](./mongodb_direct_guide.md).
|
|
@@ -95,7 +103,7 @@ traces. The wire payload itself is unchanged.
|
|
|
95
103
|
|
|
96
104
|
## Registering an embedding provider
|
|
97
105
|
|
|
98
|
-
`Parse::Embeddings` is a pluggable registry. v5.
|
|
106
|
+
`Parse::Embeddings` is a pluggable registry. v5.1 ships seven built-in
|
|
99
107
|
providers:
|
|
100
108
|
|
|
101
109
|
* `Parse::Embeddings::OpenAI` — text-only. `text-embedding-3-small`
|
|
@@ -106,16 +114,18 @@ providers:
|
|
|
106
114
|
`embed-multilingual-v3.0`, and `-light-v3.0` siblings; 1024 / 384 dim)
|
|
107
115
|
plus `embed-v4.0` (1536 native, 128k token context, Matryoshka-
|
|
108
116
|
truncatable to {256, 512, 1024, 1536} via `dimensions:`). `embed-v4.0`
|
|
109
|
-
is Cohere's text+image multimodal endpoint
|
|
110
|
-
|
|
111
|
-
|
|
117
|
+
is Cohere's text+image multimodal endpoint; the text path routes
|
|
118
|
+
through `/v1/embed` and the v5.1 image path routes through
|
|
119
|
+
`/v2/embed` with OpenAI-style nested
|
|
120
|
+
`{ type: "image_url", image_url: { url: ... } }` content rows.
|
|
112
121
|
* `Parse::Embeddings::Voyage` — voyage-4 family (`voyage-4-large` 2048,
|
|
113
122
|
Matryoshka; `voyage-4` 1024; `voyage-4-lite` 512; `voyage-4-nano` 256),
|
|
114
123
|
voyage-3 family, domain models (`voyage-code-3`, `voyage-finance-2`,
|
|
115
124
|
`voyage-law-2`), and `voyage-multimodal-3` (1024-dim, 32k token
|
|
116
125
|
context, routes to `/v1/multimodalembeddings` with the wrapped
|
|
117
|
-
`{inputs: [{content: [{type: "text", text: ...}]}]}` envelope
|
|
118
|
-
|
|
126
|
+
`{inputs: [{content: [{type: "text", text: ...}]}]}` envelope for
|
|
127
|
+
text and `{type: "image_url", image_url: <url>}` content rows for
|
|
128
|
+
the v5.1 `embed_image` path).
|
|
119
129
|
* `Parse::Embeddings::Jina` — `jina-embeddings-v3` (1024, Matryoshka
|
|
120
130
|
32–1024), `jina-embeddings-v4` (2048, Matryoshka), v5 family
|
|
121
131
|
(`jina-embeddings-v5-text-{small,nano}`,
|
|
@@ -339,7 +349,7 @@ Mechanics:
|
|
|
339
349
|
skipped. If every source is blank, the target and digest are both
|
|
340
350
|
cleared on save.
|
|
341
351
|
|
|
342
|
-
### Single vector per record
|
|
352
|
+
### Single vector per record
|
|
343
353
|
|
|
344
354
|
`embed` produces exactly one vector per record. There is no built-in
|
|
345
355
|
chunker. Long source text whose concatenation exceeds the provider's
|
|
@@ -347,7 +357,7 @@ per-call token budget will be truncated provider-side, and the
|
|
|
347
357
|
resulting vector will represent only the leading portion of the
|
|
348
358
|
document.
|
|
349
359
|
|
|
350
|
-
For long-form content
|
|
360
|
+
For long-form content, two options:
|
|
351
361
|
|
|
352
362
|
1. **Pre-chunk client-side** and write each chunk as its own
|
|
353
363
|
`Parse::Object` record with its own `embed` declaration.
|
|
@@ -357,16 +367,102 @@ For long-form content in v5.0, two options:
|
|
|
357
367
|
parents as needed.
|
|
358
368
|
|
|
359
369
|
A built-in chunker plus a `semantic_search` agent tool are scheduled
|
|
360
|
-
for
|
|
370
|
+
for a future release.
|
|
361
371
|
|
|
362
|
-
|
|
372
|
+
---
|
|
373
|
+
|
|
374
|
+
## Image embedding: `embed_image` macro (v5.1)
|
|
375
|
+
|
|
376
|
+
`embed_image` is the image-source counterpart to `embed`. The source
|
|
377
|
+
property must be `:file`-typed; the target must be a `:vector` property
|
|
378
|
+
whose declared `provider:` supports multimodal input (currently
|
|
379
|
+
`:voyage` with `voyage-multimodal-3`, or `:cohere` with `embed-v4.0`).
|
|
380
|
+
|
|
381
|
+
```ruby
|
|
382
|
+
class Post < Parse::Object
|
|
383
|
+
property :cover_image, :file
|
|
384
|
+
property :cover_image_embedding, :vector,
|
|
385
|
+
dimensions: 1024,
|
|
386
|
+
provider: :voyage,
|
|
387
|
+
model: "voyage-multimodal-3"
|
|
388
|
+
|
|
389
|
+
embed_image :cover_image, into: :cover_image_embedding
|
|
390
|
+
end
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
### Operator setup (required before any save)
|
|
394
|
+
|
|
395
|
+
Image embedding hands an attacker-influenced URL (a user-uploaded
|
|
396
|
+
`Parse::File`, a chat message, an agent tool argument) to a third-party
|
|
397
|
+
provider that will issue an HTTP request from its own network. The
|
|
398
|
+
provider's fetch happens after SDK-side validation, so DNS rebinding
|
|
399
|
+
and redirect-following are residual risks the SDK cannot eliminate.
|
|
400
|
+
|
|
401
|
+
The setup must happen in this exact order — skipping (1) or (2) raises
|
|
402
|
+
a typed error at save time with a message naming the missing
|
|
403
|
+
prerequisite:
|
|
404
|
+
|
|
405
|
+
```ruby
|
|
406
|
+
# (1) Declare which CDNs the validator will accept. Empty allowlist
|
|
407
|
+
# denies every host — opposite of Parse::File.allowed_remote_hosts.
|
|
408
|
+
Parse::Embeddings.allowed_image_hosts = [
|
|
409
|
+
".cloudfront.net", # suffix match (leading ".")
|
|
410
|
+
"files.example.com", # exact match
|
|
411
|
+
]
|
|
412
|
+
|
|
413
|
+
# (2) Sentinel-gated opt-in. Only the exact frozen String unlocks;
|
|
414
|
+
# `true`, `"true"`, `1`, or any other value raises
|
|
415
|
+
# Parse::Embeddings::ConfirmationRequired.
|
|
416
|
+
Parse::Embeddings.trust_provider_url_fetch = "PROVIDER_EGRESS_VERIFIED"
|
|
417
|
+
|
|
418
|
+
# (3) Declare embed_image on the model.
|
|
419
|
+
class Post < Parse::Object
|
|
420
|
+
embed_image :cover_image, into: :cover_image_embedding
|
|
421
|
+
end
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
### URL validator (`Parse::Embeddings.validate_image_url!`)
|
|
425
|
+
|
|
426
|
+
Every `embed_image` save path routes through
|
|
427
|
+
`Parse::Embeddings.validate_image_url!(url, allow_insecure:)`, which
|
|
428
|
+
runs layered cheap-first checks: sentinel set, `https://` (or
|
|
429
|
+
`http://` with `allow_insecure: true`), no userinfo, host not an
|
|
430
|
+
obfuscated-IP form (`0x7f.0.0.1`, `127.1`, `2130706433`), host in the
|
|
431
|
+
allowlist, port in `Parse::File.allowed_remote_ports`, host resolves
|
|
432
|
+
only to public addresses (delegated to
|
|
433
|
+
`Parse::File.assert_host_allowed!` so the SSRF mechanism is shared
|
|
434
|
+
with `Parse::File`, not parallelized). Failures raise
|
|
435
|
+
`Parse::Embeddings::InvalidImageURL` with a `:reason` Symbol
|
|
436
|
+
(`:scheme`, `:port`, `:userinfo`, `:host_blocked`,
|
|
437
|
+
`:host_not_allowlisted`, `:parse`).
|
|
438
|
+
|
|
439
|
+
### Save-side semantics
|
|
440
|
+
|
|
441
|
+
* Digest is the **SHA-256 of the URL String**, not the file bytes.
|
|
442
|
+
Replacing the `Parse::File` with one pointing at a different URL
|
|
443
|
+
re-embeds; resaving the same URL is a no-op (zero provider calls).
|
|
444
|
+
Parse-managed file URLs are stable unless overwritten in place — if
|
|
445
|
+
you PUT-replace bytes at the same URL (S3 without renaming), null
|
|
446
|
+
the digest field to force re-embed.
|
|
447
|
+
* The same `EmbedManaged` write-guard applies: direct assignment to
|
|
448
|
+
the managed vector raises `ProtectedFieldError`. The write path is
|
|
449
|
+
the only way to populate the target vector.
|
|
450
|
+
* `embed` and `embed_image` can co-declare on the same record
|
|
451
|
+
(different source properties → different `:vector` targets), so a
|
|
452
|
+
record can have one text-embedding column and one image-embedding
|
|
453
|
+
column queried by separate Atlas vectorSearch indexes.
|
|
454
|
+
|
|
455
|
+
---
|
|
456
|
+
|
|
457
|
+
## Re-embedding existing rows
|
|
363
458
|
|
|
364
459
|
Changing `model:`, `dimensions:`, or `provider:` on an existing
|
|
365
|
-
`:vector` property is a migration
|
|
460
|
+
`:vector` property is a migration regardless of whether the source is
|
|
461
|
+
text or images. Workflow:
|
|
366
462
|
|
|
367
463
|
1. Add the new property alongside the old one
|
|
368
|
-
(`property :body_embedding_v2, :vector, ...`) and an `embed`
|
|
369
|
-
targeting it.
|
|
464
|
+
(`property :body_embedding_v2, :vector, ...`) and an `embed` or
|
|
465
|
+
`embed_image` block targeting it.
|
|
370
466
|
2. Backfill: iterate existing rows, force a save (or null+save) to
|
|
371
467
|
trigger the new directive. The old field stays valid for reads.
|
|
372
468
|
3. Once backfill completes, deploy a new vectorSearch index covering
|
|
@@ -374,7 +470,10 @@ Changing `model:`, `dimensions:`, or `provider:` on an existing
|
|
|
374
470
|
4. Drop the old property.
|
|
375
471
|
|
|
376
472
|
Do NOT mutate the model in place — the digest mechanism will see
|
|
377
|
-
unchanged source text and skip recompute,
|
|
473
|
+
unchanged source text / unchanged source URL and skip recompute,
|
|
474
|
+
leaving stale vectors. For `embed_image`, also remember the digest is
|
|
475
|
+
over the URL String: if you replace bytes at the same URL (PUT-replace
|
|
476
|
+
on S3 without renaming), null the digest field to force re-embed.
|
|
378
477
|
|
|
379
478
|
---
|
|
380
479
|
|
|
@@ -491,7 +590,8 @@ on every poll) rather than a `until index_ready?; sleep` loop.
|
|
|
491
590
|
Key files:
|
|
492
591
|
|
|
493
592
|
* `lib/parse/embeddings.rb` — registry, `Configuration`, `register`,
|
|
494
|
-
`provider`, `configure
|
|
593
|
+
`provider`, `configure`, `validate_image_url!`,
|
|
594
|
+
`trust_provider_url_fetch=`, `allowed_image_hosts=`.
|
|
495
595
|
* `lib/parse/embeddings/provider.rb` — abstract base, `validate_response!`,
|
|
496
596
|
`instrument_embed`, AS::N payload contract.
|
|
497
597
|
* `lib/parse/embeddings/openai.rb` — OpenAI provider.
|
|
@@ -504,7 +604,8 @@ Key files:
|
|
|
504
604
|
local-gateway client.
|
|
505
605
|
* `lib/parse/embeddings/fixture.rb` — deterministic test provider.
|
|
506
606
|
* `lib/parse/model/core/vector_searchable.rb` — `find_similar`.
|
|
507
|
-
* `lib/parse/model/core/embed_managed.rb` — `embed`
|
|
607
|
+
* `lib/parse/model/core/embed_managed.rb` — `embed` and `embed_image`
|
|
608
|
+
macros, `EmbedDirective` (carries `modality:`, `allow_insecure:`).
|
|
508
609
|
* `lib/parse/vector_search.rb` — low-level `Parse::VectorSearch.search`.
|
|
509
610
|
* `lib/parse/atlas_search/index_manager.rb` — `IndexCatalog.create_index`,
|
|
510
611
|
`find_vector_index`, `wait_for_ready`.
|
data/docs/client_sdk_guide.md
CHANGED
|
@@ -504,6 +504,13 @@ server-side guidance.
|
|
|
504
504
|
|
|
505
505
|
## 4. ACL — the row-level boundary
|
|
506
506
|
|
|
507
|
+
> **For the full ACL + CLP reference**, including aggregate-query
|
|
508
|
+
> enforcement asymmetry, Atlas Search, mongo-direct, role hierarchy
|
|
509
|
+
> direction, `protectedFields` semantics including the `_User`
|
|
510
|
+
> owner-exempt trap, and field-guard write protection, see
|
|
511
|
+
> [`acl_clp_guide.md`](./acl_clp_guide.md). The sections below are
|
|
512
|
+
> a client-mode quickstart.
|
|
513
|
+
|
|
507
514
|
Parse Server enforces ACL on every read and write against a non-master
|
|
508
515
|
caller. The SDK's job is to (a) thread the session token in so the server
|
|
509
516
|
has someone to check against, and (b) compose ACLs correctly on the
|
|
@@ -707,7 +714,74 @@ Both `Parse.client.fetch_object` and `Parse::Query#results` strip the
|
|
|
707
714
|
protected field; the SDK doesn't try to re-synthesize it from any
|
|
708
715
|
cache. If you see it in your client-side result, your CLP is wrong.
|
|
709
716
|
|
|
710
|
-
|
|
717
|
+
Reads are stripped today; the **write** response historically still
|
|
718
|
+
echoed the value back in the create/update reply. Parse Server's
|
|
719
|
+
`protectedFieldsSaveResponseExempt` option closes that — its default
|
|
720
|
+
**will change to `false`** in a future version, which strips
|
|
721
|
+
`protectedFields` from write responses too. Set
|
|
722
|
+
`protectedFieldsSaveResponseExempt: false` in your server config to opt
|
|
723
|
+
in early. The SDK needs no changes: `save` merges the response onto the
|
|
724
|
+
object (it only overwrites fields the reply contains), so a stripped
|
|
725
|
+
protected field keeps its locally-assigned value rather than being
|
|
726
|
+
nulled out.
|
|
727
|
+
|
|
728
|
+
### 6.3 `_Installation` is special — CLP can't override the hardcoded gates
|
|
729
|
+
|
|
730
|
+
Parse Server hardcodes the access policy for `_Installation` at the REST
|
|
731
|
+
layer. CLP on this class is a thin overlay on top of behavior that is
|
|
732
|
+
already constrained, so `set_clp` (or a server-side CLP edit via the
|
|
733
|
+
Dashboard) can only tighten the operations Parse Server lets you
|
|
734
|
+
configure — it cannot loosen the ones the server pins to master-only.
|
|
735
|
+
|
|
736
|
+
| Operation | What's actually enforced |
|
|
737
|
+
|------------|------------------------------------------------------------------------------------------|
|
|
738
|
+
| `find` | **Master key only. Hardcoded.** CLP changes are ignored by the server. |
|
|
739
|
+
| `delete` | **Master key only. Hardcoded.** CLP changes are ignored by the server. |
|
|
740
|
+
| `create` | Open to anonymous clients — `X-Parse-Installation-Id` is the credential. |
|
|
741
|
+
| `update` | Open when the request's `installationId` matches the record; else master key. |
|
|
742
|
+
| `get` | CLP applies normally. |
|
|
743
|
+
| `count` | CLP applies normally. |
|
|
744
|
+
| `addField` | CLP applies normally. |
|
|
745
|
+
|
|
746
|
+
Safe to tighten:
|
|
747
|
+
|
|
748
|
+
* `get` → `requiresAuthentication: true` or master-only. SDKs don't
|
|
749
|
+
normally GET their own installation from the server; they cache
|
|
750
|
+
`currentInstallation` locally.
|
|
751
|
+
* `count` → master-only. The push flow doesn't need it, and it removes a
|
|
752
|
+
small enumeration signal.
|
|
753
|
+
* `addField` → master-only. Good hardening default for any class.
|
|
754
|
+
* `protectedFields` → hide `deviceToken`, `GCMSenderId`, `pushType` from
|
|
755
|
+
non-master reads. These are write-only from the client's perspective
|
|
756
|
+
in normal SDK flows.
|
|
757
|
+
|
|
758
|
+
Do NOT tighten:
|
|
759
|
+
|
|
760
|
+
* `create` requiring authentication — breaks first-launch device
|
|
761
|
+
registration for users who haven't logged in yet. If your app pushes
|
|
762
|
+
to anonymous users, this kills it.
|
|
763
|
+
* `update` requiring authentication — breaks silent device-token
|
|
764
|
+
refresh and channel subscribe/unsubscribe before login.
|
|
765
|
+
* Pointer-based `readUserFields` / `writeUserFields` on `_Installation`
|
|
766
|
+
— a device has no stable owning user (it can outlive a session and
|
|
767
|
+
change users), so user-pointer ACLing is unreliable.
|
|
768
|
+
* Anything on `find` / `delete` — the server ignores it.
|
|
769
|
+
|
|
770
|
+
If your app genuinely requires login before any installation write, put
|
|
771
|
+
the policy in a `beforeSave('_Installation')` Cloud Code trigger rather
|
|
772
|
+
than in CLP:
|
|
773
|
+
|
|
774
|
+
```js
|
|
775
|
+
Parse.Cloud.beforeSave('_Installation', ({ user, master }) => {
|
|
776
|
+
if (!master && !user) throw 'login required';
|
|
777
|
+
});
|
|
778
|
+
```
|
|
779
|
+
|
|
780
|
+
The trigger fires under master-key context and can inspect `request.user`
|
|
781
|
+
directly without disturbing the anonymous registration handshake that
|
|
782
|
+
the client SDKs depend on.
|
|
783
|
+
|
|
784
|
+
### 6.4 ACL still applies under CLP
|
|
711
785
|
|
|
712
786
|
CLP says "is this class operation allowed at all?". ACL says "given the
|
|
713
787
|
operation is allowed, which rows does this caller see / touch?". An
|
|
@@ -715,6 +789,104 @@ authed user who passed the CLP gate still gets their result set filtered
|
|
|
715
789
|
by ACL — if Alice writes a row with `acl.apply(alice.id, true, true)`
|
|
716
790
|
only, Bob's query for it (under his own session) returns nothing.
|
|
717
791
|
|
|
792
|
+
### 6.5 The other system classes — where CLP isn't the whole story
|
|
793
|
+
|
|
794
|
+
`_Installation` (section 6.3) isn't unique. Several Parse Server system
|
|
795
|
+
classes either ignore CLP entirely or layer it under hardcoded behavior.
|
|
796
|
+
Treat this table as the authoritative answer for "what can I actually
|
|
797
|
+
configure here?":
|
|
798
|
+
|
|
799
|
+
| Class | Does CLP do anything? |
|
|
800
|
+
|--------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------|
|
|
801
|
+
| `_User` | Yes, but layered under hardcoded protections (password never returned, `authData` stripped from non-master finds, unauth update requires matching session token, email/username lowercasing, owner-exempt `protectedFields`). |
|
|
802
|
+
| `_Role` | Yes, layered under role-name regex, relation validation, and hierarchy integrity checks. |
|
|
803
|
+
| `_Installation` | Partial — only `get`, `count`, `addField`, and `protectedFields` are configurable; `find` and `delete` are master-only regardless of CLP. See section 6.3. |
|
|
804
|
+
| `_Session` | Mostly redundant — non-master queries are silently rewritten to `{ user: <current user> }` (`RestQuery.js`), so a caller only ever sees their own sessions. `find` also requires a session token. |
|
|
805
|
+
| `_JobStatus`, `_PushStatus`, `_Hooks`, `_GlobalConfig`, `_GraphQLConfig`, `_JobSchedule`, `_Audience`, `_Idempotency`, `_Join:*` (all relation join tables) | No — master-key-only at the REST layer (`SharedRest.js`). CLP changes are ignored. |
|
|
806
|
+
|
|
807
|
+
Practical consequences when you're building a client-mode app:
|
|
808
|
+
|
|
809
|
+
* Don't query `_JobStatus`, `_PushStatus`, `_Audience`, `_JobSchedule`,
|
|
810
|
+
or any `_Join:*` table from the client — those calls require master
|
|
811
|
+
key. In the SDK, the corresponding model classes
|
|
812
|
+
(`Parse::JobStatus`, `Parse::PushStatus`, `Parse::Audience`,
|
|
813
|
+
`Parse::JobSchedule`) are server-side helpers. Auto-promote to a
|
|
814
|
+
master-key client or expose them through a Cloud Code function.
|
|
815
|
+
* On `_Session`, you don't need ACLs or CLP to scope queries to the
|
|
816
|
+
caller — Parse Server already does that. You also can't grant a user
|
|
817
|
+
visibility into another user's sessions through CLP.
|
|
818
|
+
* On `_User`, never assume CLP alone gates a flow. Password changes,
|
|
819
|
+
email verification, and session-token rotation have their own paths
|
|
820
|
+
in `RestWrite.js`; they fire even if your CLP looks restrictive.
|
|
821
|
+
* On `_Role`, the role graph is validated server-side. CLP can gate
|
|
822
|
+
who can call create/update/delete, but the contents of the `roles`
|
|
823
|
+
and `users` relations are still checked for cycles and bad names.
|
|
824
|
+
|
|
825
|
+
### 6.6 `_User` field visibility — master-only vs. self-only
|
|
826
|
+
|
|
827
|
+
Two patterns come up constantly on `_User`:
|
|
828
|
+
|
|
829
|
+
* **Master-only fields** — admin-side metadata that the user themselves
|
|
830
|
+
should not see. Examples: `my_opinion_of_them`, `risk_score`,
|
|
831
|
+
`moderation_notes`.
|
|
832
|
+
* **Self-visible fields** — private profile data the user should see
|
|
833
|
+
on their own row, but nobody else. Examples: `favorite_color`,
|
|
834
|
+
`private_notes`, full `email`.
|
|
835
|
+
|
|
836
|
+
Vanilla `protectedFields` doesn't express either cleanly on `_User`,
|
|
837
|
+
because Parse Server's `protectedFieldsOwnerExempt` option (historical
|
|
838
|
+
default **true**) silently exempts the owning user from every
|
|
839
|
+
`protectedFields` rule on `_User`. So if you write `protect_fields "*",
|
|
840
|
+
[:risk_score]`, the user still sees their own `risk_score`. (Parse
|
|
841
|
+
Server's default for this option **is changing to `false`** in a future
|
|
842
|
+
version; until your server adopts that default, set it explicitly as in
|
|
843
|
+
step 1.) The fix has two moving parts on the server:
|
|
844
|
+
|
|
845
|
+
1. Start Parse Server with `protectedFieldsOwnerExempt: false`.
|
|
846
|
+
2. Add a self-pointer field on `_User` and populate it from a Cloud
|
|
847
|
+
Code trigger so each row points at itself:
|
|
848
|
+
|
|
849
|
+
```js
|
|
850
|
+
Parse.Cloud.beforeSave(Parse.User, (req) => {
|
|
851
|
+
const u = req.object;
|
|
852
|
+
if (!u.get('self')) u.set('self', u);
|
|
853
|
+
});
|
|
854
|
+
```
|
|
855
|
+
|
|
856
|
+
With those in place, the SDK exposes a small DSL on `Parse::User`:
|
|
857
|
+
|
|
858
|
+
```ruby
|
|
859
|
+
class Parse::User
|
|
860
|
+
property :my_opinion_of_them, :string
|
|
861
|
+
property :favorite_color, :string
|
|
862
|
+
|
|
863
|
+
master_only_fields :my_opinion_of_them
|
|
864
|
+
self_visible_fields :favorite_color, via: :self # name of the self-pointer
|
|
865
|
+
end
|
|
866
|
+
```
|
|
867
|
+
|
|
868
|
+
That expands to:
|
|
869
|
+
|
|
870
|
+
```ruby
|
|
871
|
+
protect_fields "*", ["myOpinionOfThem", "favoriteColor"]
|
|
872
|
+
protect_fields "userField:self", ["myOpinionOfThem"]
|
|
873
|
+
```
|
|
874
|
+
|
|
875
|
+
Resolution (recall: matching groups intersect):
|
|
876
|
+
|
|
877
|
+
| Caller | Matching groups | Hidden (intersection) | Visible |
|
|
878
|
+
|----------------|----------------------------|-------------------------------------|---------------|
|
|
879
|
+
| Other user | `*` | `myOpinionOfThem`, `favoriteColor` | neither |
|
|
880
|
+
| The user itself| `*` ∩ `userField:self` | `myOpinionOfThem` only | `favoriteColor` |
|
|
881
|
+
| Master key | none (master bypasses) | nothing | both |
|
|
882
|
+
|
|
883
|
+
If your code uses raw `protect_fields` on `_User` directly, the SDK
|
|
884
|
+
emits a one-time advisory pointing at these helpers and reminding you
|
|
885
|
+
to set `protectedFieldsOwnerExempt: false`. You can still use the raw
|
|
886
|
+
form — the override calls through — but the warning is there because
|
|
887
|
+
the default Parse Server setting will silently negate a lot of what
|
|
888
|
+
the raw `protect_fields` calls look like they're doing.
|
|
889
|
+
|
|
718
890
|
---
|
|
719
891
|
|
|
720
892
|
## 7. Files
|
|
@@ -966,11 +1138,35 @@ server-side on every event before it goes out the WebSocket — Bob will
|
|
|
966
1138
|
not receive an event for an ACL-private row Alice creates, even if his
|
|
967
1139
|
subscription matches the `where` clause.
|
|
968
1140
|
|
|
1141
|
+
> **Master-key authorization is per-CONNECTION, not per-subscription.**
|
|
1142
|
+
> Parse Server resolves master-key (ACL/CLP-bypass) authorization once,
|
|
1143
|
+
> from the connect frame; once set, EVERY subscription on that socket
|
|
1144
|
+
> bypasses ACL/CLP. The SDK therefore keeps connections **ACL-scoped by
|
|
1145
|
+
> default**: a configured `master_key` does NOT elevate the connection.
|
|
1146
|
+
> To build an admin (ACL-bypassing) connection — an event tap that sees
|
|
1147
|
+
> every row regardless of ACL — opt in explicitly:
|
|
1148
|
+
>
|
|
1149
|
+
> ```ruby
|
|
1150
|
+
> admin = Parse::LiveQuery::Client.new(
|
|
1151
|
+
> url: "wss://parse.example.com/parse",
|
|
1152
|
+
> application_id: "MY_APP_ID",
|
|
1153
|
+
> master_key: ENV["PARSE_MASTER_KEY"],
|
|
1154
|
+
> use_master_key: true, # whole connection bypasses ACL/CLP; warns at connect
|
|
1155
|
+
> )
|
|
1156
|
+
> ```
|
|
1157
|
+
>
|
|
1158
|
+
> There is no per-subscription master key — `subscribe(use_master_key: true)`
|
|
1159
|
+
> on a scoped connection warns and stays ACL-scoped. For a process that
|
|
1160
|
+
> needs both scoped and admin streams, use two separate clients. Use
|
|
1161
|
+
> `client.admin_connection?` to check whether a connection is elevated.
|
|
1162
|
+
|
|
969
1163
|
> **Configuration tip.** `Parse::LiveQuery::Client.new` reads
|
|
970
|
-
> `master_key` from configuration if you omit it.
|
|
971
|
-
> **explicitly** in client builds
|
|
972
|
-
>
|
|
973
|
-
>
|
|
1164
|
+
> `master_key` from configuration if you omit it. Passing
|
|
1165
|
+
> `master_key: nil` **explicitly** in client builds is still good
|
|
1166
|
+
> hygiene (the SDK preserves a sentinel so it can tell "not provided"
|
|
1167
|
+
> apart from "explicitly nil"), but note that as of v5.1.0 a present
|
|
1168
|
+
> master key alone no longer elevates a LiveQuery connection — only
|
|
1169
|
+
> `use_master_key: true` does.
|
|
974
1170
|
|
|
975
1171
|
---
|
|
976
1172
|
|
data/docs/usage_guide.md
CHANGED
|
@@ -394,6 +394,27 @@ Song.query(tags: tag).results # songs containing this tag
|
|
|
394
394
|
tag.songs.results # inverse query, if Tag declares has_many :songs, through: :relation
|
|
395
395
|
```
|
|
396
396
|
|
|
397
|
+
### Heads up: Parse Server request-complexity limits
|
|
398
|
+
|
|
399
|
+
Recent Parse Server versions add `requestComplexity` limits whose
|
|
400
|
+
defaults are changing from "unlimited" (`-1`) to finite values in a
|
|
401
|
+
future release: `includeDepth: 10`, `includeCount: 100`,
|
|
402
|
+
`subqueryDepth: 10`, `queryDepth: 10`, and `batchRequestLimit: 100`.
|
|
403
|
+
These cap how deep an `includes:` chain can nest, how many include
|
|
404
|
+
paths a single query may carry, how deeply `matches_query` /
|
|
405
|
+
`$inQuery` / `$select` subqueries nest, how deeply `$and` / `$or`
|
|
406
|
+
conditions nest, and how many sub-requests a batch may contain.
|
|
407
|
+
|
|
408
|
+
The SDK's defaults stay within these limits — most relevantly, the
|
|
409
|
+
batch segment size is **50** (`Parse::BatchOperation#submit`), under the
|
|
410
|
+
incoming `batchRequestLimit: 100`. The cases to watch are app-specific:
|
|
411
|
+
very deep `includes: [{a: {b: {c: …}}}]` chains, queries with many
|
|
412
|
+
distinct include paths, or deeply nested subqueries can start returning
|
|
413
|
+
errors once the finite defaults land. If you hit one, restructure the
|
|
414
|
+
query (split it, fetch pointers lazily, flatten the nesting) or raise
|
|
415
|
+
the specific `requestComplexity.*` limit on your server. Set any of them
|
|
416
|
+
to `-1` to opt out of that limit entirely.
|
|
417
|
+
|
|
397
418
|
## Atomic Operations
|
|
398
419
|
|
|
399
420
|
Use atomic ops to avoid read-modify-write races on counters, sets, and
|