@sanity-labs/client-fake-for-test 0.11.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/dist/index.d.cts CHANGED
@@ -38,7 +38,7 @@ export declare function applyPatchOps(
38
38
  */
39
39
  export declare function applyPerspective(
40
40
  docs: readonly SanityDocument[],
41
- perspective: Perspective,
41
+ requested: Perspective,
42
42
  ): SanityDocument[];
43
43
 
44
44
  /**
@@ -61,6 +61,8 @@ export declare interface CallRecord {
61
61
  tag?: string | null;
62
62
  projectId: string;
63
63
  dataset: string;
64
+ /** The resource the call targeted, on a `resource`-configured handle. */
65
+ resource?: TestClientResource;
64
66
  }
65
67
 
66
68
  /** Outcome of a recorded call, reported once a (possibly async) result is known. */
@@ -105,6 +107,27 @@ export declare interface CreateTestClientOptions {
105
107
  * pairs. Useful when workflows span multiple datasets.
106
108
  */
107
109
  datasets?: Record<string, SanityDocumentLike[]>;
110
+ /**
111
+ * Optional: seed any resource type. Keys are `"<type>:<id>"`, the
112
+ * `resource` config's two fields: `"media-library:ml1"`, `"canvas:c1"`,
113
+ * `"dashboard:d1"`, or `"dataset:<projectId>.<dataset>"` (the same store
114
+ * as `datasets["<projectId>:<dataset>"]`).
115
+ */
116
+ resources?: Record<string, SanityDocumentLike[]>;
117
+ /**
118
+ * The resource the root handle points at, named exactly as
119
+ * `@sanity/client`'s `resource` config names it. While set, it decides
120
+ * the store (over `projectId` / `dataset`), and each capability the
121
+ * platform lacks on that resource type answers with the platform's
122
+ * refusal. `withConfig({ resource })` mints siblings on other resources.
123
+ */
124
+ resource?: TestClientResource;
125
+ /**
126
+ * The organization every resource of this client belongs to — what
127
+ * `GET /media-libraries/<id>`, `/canvases/<id>`, `/dashboards/<id>` and
128
+ * `/projects/<projectId>` answer as `organizationId`.
129
+ */
130
+ organizationId?: string;
108
131
  /**
109
132
  * Opt-in access-control simulation. When set, the client exposes
110
133
  * `request({ url })` that serves `/users/me` (host-aware), the
@@ -152,12 +175,12 @@ export declare interface CreateTestClientOptions {
152
175
  */
153
176
  token?: string;
154
177
  /**
155
- * Pluggable write interceptor — runs on each write through the document
156
- * mutation API (create/patch/delete/transaction; not the release/version
157
- * action API) before the store changes, and may throw to reject it. The
158
- * client has no opinion about the policy; callers (e.g. a workflow test bench
159
- * enforcing lake mutation guards) supply it. Inherited by `withConfig`
160
- * siblings. See {@link MutationGuard}.
178
+ * Pluggable write interceptor — runs once for each document's net change in
179
+ * a commit. Document publish/unpublish actions expose their underlying
180
+ * draft/published mutations; release/version actions remain unhooked. Return
181
+ * denial descriptors to reject atomically with `GuardDeniedError`; a
182
+ * thrown callback error propagates unchanged. The client owns no policy.
183
+ * Inherited by `withConfig` siblings. See {@link MutationGuard}.
161
184
  */
162
185
  mutationGuard?: MutationGuard;
163
186
  /**
@@ -246,7 +269,11 @@ export declare interface DocumentBody {
246
269
  [field: string]: unknown;
247
270
  }
248
271
 
249
- export declare type DocumentValuePermission = "create" | "read" | "update";
272
+ export declare type DocumentValuePermission =
273
+ | "create"
274
+ | "manage"
275
+ | "read"
276
+ | "update";
250
277
 
251
278
  /**
252
279
  * Error helpers shared across the fake.
@@ -326,11 +353,14 @@ export declare type FakeMutation =
326
353
  /**
327
354
  * Data-plane subset of `@sanity/client`'s surface, with multi-dataset support.
328
355
  *
329
- * One `createTestClient()` call creates a registry of stores keyed by
330
- * `(projectId, dataset)`. The returned client points at one of them.
331
- * `withConfig({ projectId, dataset })` returns a new handle into the same
332
- * registry but pointing at a different store — useful for workflows where
333
- * definitions live in one dataset and instances live in another.
356
+ * One `createTestClient()` call creates a registry of stores, one per
357
+ * resource: a `(projectId, dataset)` pair, or a Media Library, Canvas or
358
+ * dashboard named by `resource: { type, id }` exactly as `@sanity/client`
359
+ * names it. The returned client points at one of them.
360
+ * `withConfig({ projectId, dataset })` or `withConfig({ resource })` returns
361
+ * a new handle into the same registry but pointing at a different store —
362
+ * useful for workflows where definitions live in one dataset and instances
363
+ * live in another.
334
364
  *
335
365
  * Two separate `createTestClient()` calls have separate registries, so
336
366
  * tests stay isolated from each other.
@@ -341,7 +371,7 @@ export declare type FakeMutation =
341
371
  * scoped to the current dataset.
342
372
  * - `simulateConcurrentEdit()` — force the next N revision-guarded writes
343
373
  * to a document to 409, for optimistic-locking retry tests.
344
- * - `datasets()` — lists known dataset keys in this registry.
374
+ * - `datasets()` / `resources()` — list the stores known in this registry.
345
375
  *
346
376
  * Methods deliberately omitted:
347
377
  *
@@ -397,6 +427,61 @@ export declare interface GuardConfig {
397
427
  recorded?: Set<string>;
398
428
  }
399
429
 
430
+ /** HTTP-shaped guard rejection matching the error exposed by `@sanity/client`. */
431
+ export declare class GuardDeniedError extends Error {
432
+ readonly statusCode = 403;
433
+ readonly type = "guardDeniedError";
434
+ readonly denials: MutationGuardDenial[];
435
+ readonly details: GuardDeniedResponse["body"]["error"];
436
+ readonly response: GuardDeniedResponse;
437
+ constructor(denials: readonly MutationGuardDenial[]);
438
+ }
439
+
440
+ export declare interface GuardDeniedResponse {
441
+ body: {
442
+ error: {
443
+ type: "guardDeniedError";
444
+ denials: MutationGuardDenial[];
445
+ };
446
+ };
447
+ url: string;
448
+ method: "POST";
449
+ headers: Record<string, string>;
450
+ statusCode: 403;
451
+ statusMessage: "Forbidden";
452
+ }
453
+
454
+ /** An event as `GET <data base>/history/events/documents/<ids>` serves it. */
455
+ export declare interface HistoryEvent {
456
+ type: "createLiveDocument" | "updateLiveDocument";
457
+ id: string;
458
+ documentId: string;
459
+ revisionId: string;
460
+ timestamp: string;
461
+ author?: string;
462
+ }
463
+
464
+ /** A transaction as `GET <data base>/history/transactions/<ids>` serves it, one per NDJSON line. */
465
+ export declare interface HistoryTransactionRecord {
466
+ id: string;
467
+ timestamp: string;
468
+ author?: string;
469
+ mutations: (
470
+ | {
471
+ create: SanityDocument;
472
+ }
473
+ | {
474
+ createOrReplace: SanityDocument;
475
+ }
476
+ | {
477
+ delete: {
478
+ id: string;
479
+ };
480
+ }
481
+ )[];
482
+ documentIDs: string[];
483
+ }
484
+
400
485
  export declare const isDraftId: (id: string) => boolean;
401
486
 
402
487
  export declare const isReleaseDocId: (id: string) => boolean;
@@ -416,72 +501,53 @@ export declare interface MultipleMutationResult {
416
501
  }
417
502
 
418
503
  /**
419
- * A pluggable write interceptor, configured via
420
- * `createTestClient({ mutationGuard })`. It runs on each write through the
421
- * document mutation API — `create` / `createOrReplace` / `createIfNotExists` /
422
- * `patch` / `delete` and the equivalent `transaction()` operations — and on the
423
- * document publish/unpublish actions (`sanity.action.document.publish` /
424
- * `.unpublish` via `client.action`) — before the store changes; **throw to
425
- * reject the write** (the throw propagates out of the client call, exactly as
426
- * a real lake rejection would). The release/version action API
427
- * (`client.releases.*`, `createVersion`, the `sanity.action.release.*` and
428
- * `*.version.*` action types) is **not** intercepted.
429
- *
430
- * The client itself has no opinion about *what* a guard is — it only provides
431
- * the seam and the before/after/identity context. Callers (e.g. a workflow test
432
- * bench) supply the policy: evaluate lake mutation guards, enforce field locks,
433
- * etc. Keeping the policy in the caller avoids duplicating guard-evaluation
434
- * logic the client has no business owning.
504
+ * Evaluates one net document change and returns every denying guard. Returning
505
+ * `void` allows observe-only hooks. A thrown value is a callback failure and
506
+ * propagates unchanged; only returned entries become {@link GuardDeniedError}.
435
507
  */
436
508
  export declare type MutationGuard = (
437
509
  context: MutationGuardContext,
438
- ) => void | Promise<void>;
510
+ ) =>
511
+ | readonly MutationGuardDenial[]
512
+ | void
513
+ | Promise<readonly MutationGuardDenial[] | void>;
439
514
 
440
- /** The kind of write a {@link MutationGuard} is asked to vet. */
441
- export declare type MutationGuardAction =
442
- | "create"
443
- | "update"
444
- | "delete"
445
- | "publish"
446
- | "unpublish";
515
+ /** The Content Lake mutation actions that a {@link MutationGuard} can match. */
516
+ export declare type MutationGuardAction = "create" | "update" | "delete";
447
517
 
448
- /**
449
- * The before/after image of a single document write, handed to a
450
- * {@link MutationGuard} before it is applied.
451
- *
452
- * - `before` is null on create and on a publish with no live published
453
- * document; `after` is null on delete and unpublish.
454
- * - On publish/unpublish both images are of the *published* document —
455
- * `after` on publish is the draft's content promoted onto the published id.
456
- * - `identity` is the acting user — the id bound to the client's token
457
- * (its `accessControl.currentUser`), or undefined when the client has no
458
- * identity configured.
459
- *
460
- * Treat `before`/`after` as read-only — they are the live store images, not
461
- * copies; a guard that mutates them corrupts the store.
462
- */
518
+ /** The before/final-after image of one net document change in a commit. */
463
519
  export declare interface MutationGuardContext {
464
520
  action: MutationGuardAction;
465
- before:
466
- | (SanityDocumentLike & {
467
- _id: string;
468
- _type: string;
469
- })
470
- | null;
471
- after:
472
- | (SanityDocumentLike & {
473
- _id: string;
474
- _type: string;
475
- })
476
- | null;
521
+ /** Original live store image. Treat it as read-only; it is not a copy. */
522
+ before: Readonly<
523
+ SanityDocumentLike & {
524
+ _id: string;
525
+ _type: string;
526
+ }
527
+ > | null;
528
+ /** Final live store image. Treat it as read-only; it is not a copy. */
529
+ after: Readonly<
530
+ SanityDocumentLike & {
531
+ _id: string;
532
+ _type: string;
533
+ }
534
+ > | null;
535
+ /** Acting project identity, or `undefined` on an identity-free client. */
477
536
  identity: string | undefined;
478
537
  }
479
538
 
539
+ export declare interface MutationGuardDenial {
540
+ guardID: string;
541
+ name?: string;
542
+ }
543
+
480
544
  /** Append-only record of one committed transaction (a single mutation is a 1-entry transaction). */
481
545
  export declare interface MutationLogEntry {
482
546
  transactionId: string;
483
547
  projectId: string;
484
548
  dataset: string;
549
+ /** The resource the transaction targeted, on a `resource`-configured handle. */
550
+ resource?: TestClientResource;
485
551
  mutations: TxnMutation[];
486
552
  /** Request tag the commit carried, when one was passed (`null` = explicit suppression). */
487
553
  tag?: string | null;
@@ -593,7 +659,46 @@ export declare type PatchOp =
593
659
  items: unknown[];
594
660
  };
595
661
 
596
- export declare type Perspective = "raw" | "published" | "drafts" | string[];
662
+ /**
663
+ * `raw`, `published`, `drafts`, a release stack, or a single release name
664
+ * (`"rSpring"`) — which the Content Lake reads as a one-release stack.
665
+ */
666
+ export declare type Perspective =
667
+ | "raw"
668
+ | "published"
669
+ | "drafts"
670
+ | (string & {})
671
+ | string[];
672
+
673
+ /** The `response` of an HTTP error, shaped like `@sanity/client`'s `HttpError["response"]`. */
674
+ declare interface PlatformErrorResponse {
675
+ body: unknown;
676
+ url: string;
677
+ method: string;
678
+ headers: Record<string, string>;
679
+ statusCode: number;
680
+ statusMessage: string;
681
+ }
682
+
683
+ /**
684
+ * The platform's refusal, shaped like the `ClientError` `@sanity/client`
685
+ * throws for the same response: `statusCode`, `response`, `responseBody`,
686
+ * `details`, and a `message` derived from the body the way the client
687
+ * derives it.
688
+ */
689
+ export declare class PlatformHttpError extends Error {
690
+ readonly statusCode: number;
691
+ readonly response: PlatformErrorResponse;
692
+ readonly responseBody: string;
693
+ readonly details: unknown;
694
+ constructor(args: {
695
+ method: string;
696
+ path: string;
697
+ statusCode: number;
698
+ statusMessage: string;
699
+ body: unknown;
700
+ });
701
+ }
597
702
 
598
703
  /** One row of the `/projects/<projectId>` members response. */
599
704
  export declare interface ProjectMemberRecord {
@@ -726,6 +831,25 @@ export declare type ReleaseState =
726
831
 
727
832
  export declare type ReleaseType = "asap" | "scheduled" | "undecided";
728
833
 
834
+ /**
835
+ * What the Access API (`/access/<type>/<id>/…`) answers about one actor on
836
+ * one resource: the roles it holds there (served by `…/users` and collected
837
+ * into `…/roles`) and the permissions `…/user-permissions/me` and
838
+ * `…/user-permissions/me/check` report, as `"<type>.<action>"` names such
839
+ * as `"sanity.media-library.read"`.
840
+ */
841
+ export declare interface ResourceAccess {
842
+ roles?: NonNullable<CurrentUser["roles"]>;
843
+ permissions?: string[];
844
+ }
845
+
846
+ /** Resource types `@sanity/client`'s `resource` config can name (the ones staging probed). */
847
+ export declare type ResourceType =
848
+ | "dataset"
849
+ | "media-library"
850
+ | "canvas"
851
+ | "dashboard";
852
+
729
853
  /** @public */
730
854
  declare interface SanityDocument {
731
855
  _id: string;
@@ -782,6 +906,7 @@ export declare const SUPPORTED_SURFACE: readonly [
782
906
  "createVersion",
783
907
  "unpublishVersion",
784
908
  "request (only with accessControl or a bound token)",
909
+ "getDataUrl",
785
910
  "config",
786
911
  "withConfig",
787
912
  ];
@@ -812,7 +937,8 @@ export declare interface TestClient {
812
937
  * registered token (`identities` + `token`). The engine's access
813
938
  * resolver pokes `/users/me` and the configured ACL path through
814
939
  * this — without it the engine throws on any verb that needs an
815
- * actor.
940
+ * actor. Also serves the resource's history, ACL, lookup and Access API
941
+ * endpoints, or the platform's refusal where the resource has none.
816
942
  */
817
943
  request?: <T>(opts: {
818
944
  url?: string;
@@ -820,6 +946,13 @@ export declare interface TestClient {
820
946
  signal?: AbortSignal;
821
947
  tag?: string | null;
822
948
  }) => Promise<T>;
949
+ /**
950
+ * `@sanity/client`'s `getDataUrl(operation, path)`: `<resource
951
+ * base>/<operation>/<path>` on a `resource`-configured handle,
952
+ * `/data/<operation>/<dataset>/<path>` otherwise. Pass the result to
953
+ * `request()` to reach e.g. the History API.
954
+ */
955
+ getDataUrl: (operation: string, path?: string) => string;
823
956
  create: <T extends SanityDocumentLike>(
824
957
  doc: T,
825
958
  options?: MutationOptions,
@@ -855,8 +988,9 @@ export declare interface TestClient {
855
988
  * Dispatch one or more release / version / document actions against the
856
989
  * store. Returns a synthetic `transactionId`. Multi-action arrays are
857
990
  * applied atomically — any failure rolls back the batch. The document
858
- * publish/unpublish actions run through the access gate and the
859
- * `mutationGuard` seam; release/version actions apply unhooked.
991
+ * publish/unpublish actions run through the access gate and expose their
992
+ * net draft/published mutations to `mutationGuard`; release/version actions
993
+ * apply unhooked.
860
994
  */
861
995
  action: (
862
996
  action: Action | Action[],
@@ -876,13 +1010,15 @@ export declare interface TestClient {
876
1010
  publishedId: string;
877
1011
  }) => Promise<ActionResult>;
878
1012
  /**
879
- * Current `(projectId, dataset)`, active perspective, and which API host
1013
+ * Current `(projectId, dataset)` or `resource`, active perspective, and which API host
880
1014
  * the handle models (`useProjectHostname`). `token` is present iff the
881
1015
  * handle is bound to a registered identity.
882
1016
  */
883
1017
  config: () => {
884
1018
  projectId: string;
885
1019
  dataset: string;
1020
+ /** Present iff the handle is `resource`-configured. */
1021
+ resource?: TestClientResource;
886
1022
  perspective: Perspective;
887
1023
  requestTagPrefix?: string;
888
1024
  token?: string;
@@ -898,6 +1034,13 @@ export declare interface TestClient {
898
1034
  config: Partial<{
899
1035
  projectId: string;
900
1036
  dataset: string;
1037
+ /**
1038
+ * Point the sibling at a resource, named as `@sanity/client`'s
1039
+ * `resource` config names it. While set, it decides the store (over
1040
+ * `projectId` / `dataset`), as in `@sanity/client`. Pass `undefined`
1041
+ * to drop it and address `(projectId, dataset)` again.
1042
+ */
1043
+ resource: TestClientResource | undefined;
901
1044
  perspective: Perspective;
902
1045
  /** Prefix composed with request tags and used alone for untagged requests. */
903
1046
  requestTagPrefix: string;
@@ -964,6 +1107,8 @@ export declare interface TestClient {
964
1107
  projectId: string;
965
1108
  dataset: string;
966
1109
  }[];
1110
+ /** List every resource known in this registry — datasets as `{ type: "dataset", id: "<projectId>.<dataset>" }`. */
1111
+ resources: () => TestClientResource[];
967
1112
  /**
968
1113
  * Register a handler for a `@sanity/client` member the fake doesn't
969
1114
  * implement (e.g. `listen`, `assets`). Accessing that member on the
@@ -1033,9 +1178,15 @@ export declare interface TestClientAccessControl {
1033
1178
  grants: Grant[];
1034
1179
  /**
1035
1180
  * URL path the engine's access resolver hits to fetch grants.
1036
- * Defaults to `/projects/<projectId>/datasets/<dataset>/acl`.
1181
+ * Defaults to the handle's resource ACL: `/projects/<projectId>/datasets/<dataset>/acl`,
1182
+ * or `<resource base>/acl` on a `resource`-configured handle.
1037
1183
  */
1038
1184
  aclPath?: string;
1185
+ /**
1186
+ * Access API entries, keyed `"<access type>:<id>"` — `"project:<projectId>"`,
1187
+ * `"media-library:<id>"`, `"canvas:<id>"`, `"dashboard:<id>"`. See {@link ResourceAccess}.
1188
+ */
1189
+ access?: Record<string, ResourceAccess>;
1039
1190
  }
1040
1191
 
1041
1192
  /**
@@ -1073,6 +1224,24 @@ export declare interface TestClientIdentity {
1073
1224
  usersByProject?: Record<string, CurrentUser>;
1074
1225
  /** Grants served at the ACL path and enforced on reads/writes. */
1075
1226
  grants: Grant[];
1227
+ /**
1228
+ * Access API entries, keyed `"<access type>:<id>"` (`"media-library:<id>"`,
1229
+ * `"canvas:<id>"`, `"dashboard:<id>"`, `"project:<projectId>"`). On a
1230
+ * non-dataset resource, an entry with `roles` makes the identity a member
1231
+ * there. Project membership needs no entry: every identity is a member of
1232
+ * every project with its project-scoped roles.
1233
+ */
1234
+ access?: Record<string, ResourceAccess>;
1235
+ }
1236
+
1237
+ /**
1238
+ * A resource, named exactly as `@sanity/client`'s `resource` config names
1239
+ * it: `{ type: "media-library", id: "ml…" }`, or `{ type: "dataset", id:
1240
+ * "<projectId>.<dataset>" }`.
1241
+ */
1242
+ export declare interface TestClientResource {
1243
+ type: ResourceType;
1244
+ id: string;
1076
1245
  }
1077
1246
 
1078
1247
  /**