@sanity-labs/client-fake-for-test 0.12.0 → 0.13.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.
package/README.md CHANGED
@@ -77,10 +77,11 @@ More patterns — passing the fake to code typed against `SanityClient`, asserti
77
77
  | Transaction | `transaction()` chain → `.commit()` | Mutations: `.create`, `.createOrReplace`, `.createIfNotExists`, `.delete`, `.patch(id, recipe)`. Atomic — any throw rolls back the whole batch. Default result is a `MultipleMutationResult` (`{ transactionId, documentIds, results }`) |
78
78
  | Actions | `action(action \| action[])` | Content Lake release / version actions, applied atomically |
79
79
  | Releases | `releases.*`, `createVersion`, `unpublishVersion` | Content Releases lifecycle — create / edit / schedule / publish / archive / delete, plus version create / replace / discard / unpublish |
80
- | Config | `config()` | Returns `{ projectId, dataset, perspective, token?, useProjectHostname }` |
81
- | Config | `withConfig({ projectId?, dataset?, perspective?, accessControl?, token?, useProjectHostname? })` | Sibling handle into the same multi-dataset registry; `token` binds a registered identity (see the token→identity registry below), `accessControl` rebinds it inline — the two are mutually exclusive; `useProjectHostname: false` mints a global-hosted sibling |
82
- | Perspectives | `raw` / `published` / `drafts` / release stacks | Per-client default or per-`fetch` override; draft/version layering with `_originalId` projection |
83
- | HTTP | `request({ url? \| uri? })` | Present **only** when the handle has an acting identity (`accessControl`, or a bound `token`). Serves `/users/me` (host-aware: project-scoped record on the project host, `globalUser` on a global-hosted handle), the configured ACL path, `/projects/<projectId>` (the project's members: `{ id, isRobot?, roles? }` rows, one per distinct resolved project user), and `/projects/<projectId>/users/<ids>` (project-user records with `id` + `sanityUserId`). Calling it without one throws a guidance error. |
80
+ | Config | `config()` | Returns `{ projectId, dataset, resource?, perspective, token?, useProjectHostname }` |
81
+ | Config | `getDataUrl(operation, path?)` | The same URL `@sanity/client` builds: `<resource base>/<operation>/<path>` on a `resource` handle, `/data/<operation>/<dataset>/<path>` otherwise |
82
+ | Config | `withConfig({ projectId?, dataset?, resource?, perspective?, accessControl?, token?, useProjectHostname? })` | Sibling handle into the same multi-resource registry; `resource` points it at a Media Library, Canvas, dashboard or dataset (see [Resource types](#resource-types-media-library-canvas-dashboard)); `token` binds a registered identity (see the token→identity registry below), `accessControl` rebinds it inline — the two are mutually exclusive; `useProjectHostname: false` mints a global-hosted sibling |
83
+ | Perspectives | `raw` / `published` / `drafts` / release stacks / a single release name or comma-joined stack (`"rA"`, `"drafts,rA"`), as `@sanity/client` sends them | Per-client default or per-`fetch` override; draft/version layering with `_originalId` projection |
84
+ | HTTP | `request({ url? \| uri? })` | Present **only** when the handle has an acting identity (`accessControl`, or a bound `token`). Serves `/users/me` (host-aware: project-scoped record on the project host, `globalUser` on a global-hosted or `resource` handle), the handle's ACL (`<data base>/acl`, or an explicit `aclPath`), `/projects/<projectId>` (the project's members: `{ id, isRobot?, roles? }` rows, one per distinct resolved project user, plus `organizationId` when configured), `/projects/<projectId>/users/<ids>` (project-user records with `id` + `sanityUserId`), the History API, resource lookup and the Access API (see [Resource types](#resource-types-media-library-canvas-dashboard)). Calling it without one throws a guidance error. |
84
85
 
85
86
  #### Mutation return shapes
86
87
 
@@ -171,7 +172,39 @@ await client.request({ uri: "/projects/alpha/users/pAlpha" }); // { id: "pAlpha"
171
172
 
172
173
  The project directory endpoint (`/projects/<projectId>`) returns each resolved project member's `id`, project-specific `roles`, and robot marker. Roles are served in canonical order, and duplicate-token agreement ignores role and property order. Registering two tokens with the same project user id is supported when their served member records agree; conflicting roles fail the request instead of making the result depend on registration order.
173
174
 
174
- Grant-filter and mutation-guard `identity()` stay bound to the **project-scoped** record, matching the lake. Identities without `globalUser` serve one record everywhere — the production robot-token shape.
175
+ Grant-filter and mutation-guard `identity()` stay bound to the **project-scoped** record on a dataset, matching the lake. On a Media Library, Canvas or dashboard there is no project, so they see the account-global record (`globalUser`, falling back to `user`). Identities without `globalUser` serve one record everywhere — the production robot-token shape.
176
+
177
+ ### Resource types: Media Library, Canvas, dashboard
178
+
179
+ A handle configured with `resource: { type, id }` — the same config `@sanity/client` takes — reads and writes that resource's own store, and answers each capability the way the platform does on that resource type (staging probe, 2026-11-06). Where the platform says no, the fake throws the platform's status and body, shaped like `@sanity/client`'s `ClientError` (`statusCode`, `message`, `response.body`, `details`).
180
+
181
+ ```ts
182
+ const client = createTestClient({
183
+ resource: { type: "media-library", id: "ml1" },
184
+ organizationId: "org1",
185
+ identities: { "t-robot": robot },
186
+ token: "t-robot",
187
+ });
188
+ const canvas = client.withConfig({ resource: { type: "canvas", id: "c1" } });
189
+ await client.request({ url: client.getDataUrl("history", `documents/a?revision=${rev}`) });
190
+ await canvas.releases.create({ releaseId: "r1" }); // 404 Cannot POST /canvases/c1/actions
191
+ ```
192
+
193
+ | Capability | dataset | media-library | canvas | dashboard |
194
+ | --- | --- | --- | --- | --- |
195
+ | `drafts.` / `versions.` ids, perspectives | ✅ | ✅ | ✅ | ✅ |
196
+ | `/actions` (releases, version and document actions) | ✅ | ✅, except `release.create` → 403 `releaseLimitExceededError` | 404 `Cannot POST …/actions` | 404 `Cannot POST …/actions` |
197
+ | History: `documents/<ids>?revision=`, `transactions/<ids>` (NDJSON), `events/documents/<ids>` | ✅ | ✅ | ✅ | 404 `Cannot GET …` |
198
+ | ACL `<data base>/acl` | ✅ | ✅ | ✅ | 404 `Cannot GET …` |
199
+ | Access API `…/user-permissions/me`, `…/me/check` | ✅ | ✅ | ✅ | ✅ |
200
+ | Access API `…/users` | ✅ | ✅ | 403 | 403 |
201
+ | Access API `…/roles` | ✅ | 403 | 403 | 403 |
202
+ | Org lookup | `/projects/<projectId>` | `GET /media-libraries/<id>` | `GET /canvases/<id>` | `GET /dashboards/<id>` |
203
+
204
+ - **Access API.** Paths are `/access/<type>/<id>/…` with type `project`, `media-library`, `canvas` or `dashboard`. An identity's `access` entries, keyed `"<type>:<id>"`, hold its `roles` there (non-dataset membership) and the `permissions` the `user-permissions` endpoints report. Every identity is a project member with its project roles. Listings come back on one page (`nextCursor: null`). An unknown type answers 400 `Invalid resource type <type>`. An id the registry does not hold (never seeded, written or addressed) answers 404 `Media library not found` / `Canvas not found` / `Dashboard not found`, except `…/me/check`, which answers `false` as staging does. The 403 cells were not proven on staging (no token could list them), so they answer the way the platform refuses a missing permission: `{ statusCode: 403, message: "You do not have permission to access this resource" }`.
205
+ - **History.** Every committed write since the client was created is recorded per resource, authored by the acting identity; seeded documents can be read at their seeded `_rev`. A handle answers for its own resource, and hides documents its grants cannot read, as reads do (a transaction that also touched such a document is served without it). Events model `createLiveDocument` / `updateLiveDocument` only.
206
+ - **Org lookup.** `createTestClient({ organizationId })` names the organization every resource of the client belongs to.
207
+ - **Identity.** `@sanity/client` sends a `resource` client's requests to the global host, so `/users/me` answers `globalUser`.
175
208
 
176
209
  ### Test-only helpers (not on real `SanityClient`)
177
210
 
@@ -185,6 +218,7 @@ Useful in test bodies, but a giveaway that the code under test isn't using the r
185
218
  | `client.reset()` | Drop everything in the current dataset |
186
219
  | `client.store()` | Direct access to the underlying `TestStore` |
187
220
  | `client.datasets()` | `{ projectId, dataset }[]` known to the registry |
221
+ | `client.resources()` | Every resource known to the registry, `{ type, id }[]` (datasets as `id: "<projectId>.<dataset>"`) |
188
222
  | `client.mutations()` | Every committed mutation, flattened and in order — assert exactly what writes your code submitted |
189
223
  | `client.transactions()` | Every committed transaction (a direct mutation is a one-mutation transaction), each tagged with `transactionId` + `(projectId, dataset)` |
190
224
  | `client.clearLog()` | Reset the mutation / transaction log |
@@ -196,6 +230,7 @@ Useful in test bodies, but a giveaway that the code under test isn't using the r
196
230
  | `client.addIdentity(token, identity)` | Register (upsert) a token→identity entry in the family-shared registry |
197
231
  | `client.identities()` | Snapshot of the registered token→identity entries |
198
232
  | `createTestClient({ datasets: { "p:d": [...] } })` | Seed multiple `(projectId, dataset)` stores up front |
233
+ | `createTestClient({ resources: { "media-library:ml1": [...] } })` | Seed any resource type up front; keys are `"<type>:<id>"` |
199
234
 
200
235
  #### Two layers of observability
201
236
 
@@ -252,7 +287,7 @@ Nested access reports the full path (`client.assets.upload(...)` → `client.ass
252
287
  | --- | --- | --- |
253
288
  | `listen(...)` — real-time | ❌ Guarded | Test subscription logic separately, or `client.stub("listen", fn)` |
254
289
  | `clone()` | ❌ Guarded | `withConfig({})` returns a sibling handle |
255
- | `getUrl(uri)` / `getDataUrl(...)` — URL builders | ❌ Guarded | Hard-code in tests, or stub |
290
+ | `getUrl(uri)` — URL builder | ❌ Guarded | Hard-code in tests, or stub |
256
291
  | `request(...)` — generic HTTP | ⚠️ Only with an acting identity | Configure `accessControl` or bind a registered `token`, stub it, or use [MSW](https://mswjs.io) |
257
292
  | `agent.*` / `assets.*` / `projects.*` / `users.*` / `live.*` / `mediaLibrary.*` | ❌ Guarded | Seed docs directly, or `client.stub(name, fn)` |
258
293
  | `observable.*` — RxJS API | ❌ Guarded | Wrap the Promise API with `from()` if needed |