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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE/bug_report.yml +105 -0
  3. data/.github/ISSUE_TEMPLATE/feature_request.yml +67 -0
  4. data/.github/dependabot.yml +13 -0
  5. data/.github/workflows/codeql.yml +1 -1
  6. data/.github/workflows/docs.yml +3 -3
  7. data/.github/workflows/release.yml +14 -3
  8. data/.github/workflows/ruby.yml +1 -1
  9. data/.gitignore +1 -0
  10. data/.yardopts +19 -0
  11. data/CHANGELOG.md +792 -0
  12. data/Gemfile +3 -0
  13. data/Gemfile.lock +8 -5
  14. data/README.md +15 -0
  15. data/Rakefile +5 -1
  16. data/docs/acl_clp_guide.md +553 -0
  17. data/docs/atlas_vector_search_guide.md +123 -22
  18. data/docs/client_sdk_guide.md +201 -5
  19. data/docs/usage_guide.md +21 -0
  20. data/docs/yard-template/default/fulldoc/html/css/common.css +1222 -0
  21. data/docs/yard-template/default/fulldoc/html/css/full_list.css +387 -0
  22. data/lib/parse/agent/tools.rb +153 -1
  23. data/lib/parse/cache/redis.rb +53 -0
  24. data/lib/parse/client/caching.rb +18 -1
  25. data/lib/parse/client.rb +79 -12
  26. data/lib/parse/embeddings/cohere.rb +143 -6
  27. data/lib/parse/embeddings/provider.rb +20 -2
  28. data/lib/parse/embeddings/voyage.rb +102 -0
  29. data/lib/parse/embeddings.rb +332 -1
  30. data/lib/parse/live_query/client.rb +167 -4
  31. data/lib/parse/live_query/configuration.rb +12 -0
  32. data/lib/parse/live_query/subscription.rb +55 -2
  33. data/lib/parse/live_query.rb +123 -1
  34. data/lib/parse/lock.rb +342 -0
  35. data/lib/parse/lock_backend.rb +308 -0
  36. data/lib/parse/model/classes/audience.rb +5 -0
  37. data/lib/parse/model/classes/installation.rb +122 -0
  38. data/lib/parse/model/classes/job_schedule.rb +3 -1
  39. data/lib/parse/model/classes/job_status.rb +4 -1
  40. data/lib/parse/model/classes/push_status.rb +4 -1
  41. data/lib/parse/model/classes/session.rb +7 -0
  42. data/lib/parse/model/classes/user.rb +204 -0
  43. data/lib/parse/model/core/create_lock.rb +28 -146
  44. data/lib/parse/model/core/embed_managed.rb +162 -13
  45. data/lib/parse/model/core/parse_reference.rb +17 -1
  46. data/lib/parse/model/core/querying.rb +26 -2
  47. data/lib/parse/model/file.rb +523 -18
  48. data/lib/parse/query.rb +31 -1
  49. data/lib/parse/stack/version.rb +1 -1
  50. data/lib/parse/stack.rb +98 -1
  51. data/parse-stack-next.gemspec +2 -2
  52. metadata +17 -7
@@ -1,11 +1,19 @@
1
1
  # Atlas Vector Search Guide
2
2
 
3
- Parse Stack v5.0 ships first-class support for MongoDB Atlas
4
- `$vectorSearch` against Parse classes. This guide covers the full
5
- surface: declaring `:vector` properties, registering embedding
6
- providers, running `find_similar` queries, the `embed` write-side
7
- macro, Atlas index management, AS::N telemetry, and the constraint and
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.0 ships seven built-in
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 at the network boundary;
110
- this release wires the **text path only** — `embed_image` lands in
111
- v5.1.
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). Text
118
- inputs only in v5.0 image content rows land in v5.1.
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 (v5.0)
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 in v5.0, two options:
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 v5.1.
370
+ for a future release.
361
371
 
362
- ### Re-embedding existing rows
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. Workflow:
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` block
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, leaving stale vectors.
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` macro.
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`.
@@ -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
- ### 6.3 ACL still applies under CLP
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. Pass `master_key: nil`
971
- > **explicitly** in client builds the SDK preserves a sentinel value
972
- > internally so it can tell "not provided" apart from "explicitly nil,"
973
- > and the latter is the only safe choice in a client context.
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