@sanity-labs/client-fake-for-test 0.2.0 → 0.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.
package/dist/index.d.ts CHANGED
@@ -35,6 +35,32 @@ export declare function applyPerspective(
35
35
  perspective: Perspective,
36
36
  ): SanityDocument[];
37
37
 
38
+ /**
39
+ * One recorded invocation of a client method — the spy layer behind
40
+ * `client.calls()` / `client.callCount()`. `result` holds the resolved
41
+ * value (awaited, for async methods); `error` holds the thrown/rejected
42
+ * value. Builder-returning methods (`patch`, `transaction`) record the
43
+ * builder as `result`; their eventual writes show up in the mutation log.
44
+ */
45
+ export declare interface CallRecord {
46
+ /** Method name; namespace methods are dotted, e.g. `releases.create`. */
47
+ method: string;
48
+ /** Arguments as passed (stored by reference, like `vi.fn`). */
49
+ args: unknown[];
50
+ /** Resolved/returned value, set once the call settles successfully. */
51
+ result?: unknown;
52
+ /** Thrown or rejected value, set if the call failed. */
53
+ error?: unknown;
54
+ projectId: string;
55
+ dataset: string;
56
+ }
57
+
58
+ /** Outcome of a recorded call, reported once a (possibly async) result is known. */
59
+ export declare type CallSettler = (outcome: {
60
+ result?: unknown;
61
+ error?: unknown;
62
+ }) => void;
63
+
38
64
  export declare function createPatch(
39
65
  documentId: string,
40
66
  executor: PatchExecutor,
@@ -142,6 +168,19 @@ export declare interface DocumentBody {
142
168
 
143
169
  export declare type DocumentValuePermission = "create" | "read" | "update";
144
170
 
171
+ /**
172
+ * Error helpers shared across the fake.
173
+ *
174
+ * Every error the fake throws is prefixed with {@link ERR_PREFIX} so it's
175
+ * unmistakably attributable to *this fake* — not to `@sanity/client`,
176
+ * groq-js, or the code under test — and carries enough context to fix
177
+ * the problem without reading the fake's source. Short, bare messages
178
+ * ("not found", "invalid id") are a poor developer experience in a test
179
+ * double: prefer stating what happened, the offending value, and what to
180
+ * do about it.
181
+ */
182
+ export declare const ERR_PREFIX = "[client-fake-for-test]";
183
+
145
184
  /**
146
185
  * Resolve whether `grants` allow `permission` on `document` for the
147
186
  * given user. Same logic the engine uses in `permissions.grantsPermissionOn`
@@ -163,6 +202,47 @@ export declare function evaluateWriteAccess(args: {
163
202
  matchingGrants: number;
164
203
  }>;
165
204
 
205
+ /** Build a prefixed Error. Pass a fully-formed, dev-facing message. */
206
+ export declare function fakeError(message: string): Error;
207
+
208
+ /**
209
+ * A single mutation in the raw array form accepted by `mutate()` — the
210
+ * same shape `@sanity/client` accepts. The inline `patch` form mirrors
211
+ * the real client's keyed patch object.
212
+ */
213
+ export declare type FakeMutation =
214
+ | {
215
+ create: SanityDocumentLike;
216
+ }
217
+ | {
218
+ createOrReplace: SanityDocumentLike;
219
+ }
220
+ | {
221
+ createIfNotExists: SanityDocumentLike;
222
+ }
223
+ | {
224
+ delete: {
225
+ id: string;
226
+ };
227
+ }
228
+ | {
229
+ patch: {
230
+ id: string;
231
+ ifRevisionID?: string;
232
+ set?: Record<string, unknown>;
233
+ setIfMissing?: Record<string, unknown>;
234
+ unset?: string[];
235
+ inc?: Record<string, number>;
236
+ dec?: Record<string, number>;
237
+ insert?: {
238
+ before?: string;
239
+ after?: string;
240
+ replace?: string;
241
+ items: unknown[];
242
+ };
243
+ };
244
+ };
245
+
166
246
  /**
167
247
  * Data-plane subset of `@sanity/client`'s surface, with multi-dataset support.
168
248
  *
@@ -188,6 +268,21 @@ export declare function evaluateWriteAccess(args: {
188
268
  export declare interface FetchOptions {
189
269
  /** Override the client's configured perspective for this read. */
190
270
  perspective?: Perspective;
271
+ /**
272
+ * When `false`, resolve to a {@link RawQueryResponse} (`{ query, ms,
273
+ * result }`) instead of the bare result — mirrors `@sanity/client`.
274
+ * Defaults to `true`.
275
+ */
276
+ filterResponse?: boolean;
277
+ /**
278
+ * With `filterResponse: false`, set to `false` to omit `query` from the
279
+ * raw response. Ignored when `filterResponse` is not `false`.
280
+ */
281
+ returnQuery?: boolean;
282
+ /** Accepted for signature parity; not simulated. */
283
+ tag?: string;
284
+ /** Accepted for signature parity; not simulated. */
285
+ signal?: AbortSignal;
191
286
  }
192
287
 
193
288
  export declare interface Grant {
@@ -195,6 +290,27 @@ export declare interface Grant {
195
290
  permissions: DocumentValuePermission[];
196
291
  }
197
292
 
293
+ export declare interface GuardConfig {
294
+ /**
295
+ * Resolve a registered stub for an unimplemented member, or `undefined`.
296
+ * Checked only for members the client doesn't already implement, so
297
+ * stubs fill gaps without shadowing core behaviour.
298
+ */
299
+ getStub: (name: string) => unknown;
300
+ /**
301
+ * Called synchronously when a recorded method is invoked, with the
302
+ * method name and its arguments. Returns a settler the wrapper invokes
303
+ * once the result (or rejection) is known. Omit to disable recording.
304
+ */
305
+ onCall?: (method: string, args: unknown[]) => CallSettler;
306
+ /**
307
+ * Top-level member names whose calls are recorded via `onCall`. A
308
+ * recorded member that resolves to a namespace object (e.g. `releases`)
309
+ * has each of its methods recorded as `name.method`.
310
+ */
311
+ recorded?: Set<string>;
312
+ }
313
+
198
314
  export declare const isDraftId: (id: string) => boolean;
199
315
 
200
316
  export declare const isReleaseDocId: (id: string) => boolean;
@@ -203,8 +319,51 @@ export declare const isSystemId: (id: string) => boolean;
203
319
 
204
320
  export declare const isVersionId: (id: string) => boolean;
205
321
 
322
+ /** Result of a mutation when ids are requested for all affected documents. */
323
+ export declare interface MultipleMutationResult {
324
+ transactionId: string;
325
+ documentIds: string[];
326
+ results: {
327
+ id: string;
328
+ operation: MutationOperation;
329
+ }[];
330
+ }
331
+
332
+ /** Append-only record of one committed transaction (a single mutation is a 1-entry transaction). */
333
+ export declare interface MutationLogEntry {
334
+ transactionId: string;
335
+ projectId: string;
336
+ dataset: string;
337
+ mutations: TxnMutation[];
338
+ }
339
+
340
+ /** The operation recorded per affected document, matching `@sanity/client`. */
341
+ export declare type MutationOperation = "create" | "update" | "delete" | "none";
342
+
206
343
  export declare interface MutationOptions {
207
344
  ifRevisionId?: string;
345
+ /**
346
+ * Shape the resolved value, mirroring `@sanity/client`:
347
+ * - default → the mutated document
348
+ * - `returnDocuments: false` → an id-shaped mutation result
349
+ * ({@link SingleMutationResult} / {@link MultipleMutationResult})
350
+ * - `returnFirst: false` with documents → an array
351
+ *
352
+ * The static return type reflects the default (the document); pass a
353
+ * type argument or cast when requesting an id-shaped result.
354
+ */
355
+ returnDocuments?: boolean;
356
+ returnFirst?: boolean;
357
+ /** Override the synthetic transaction id used in the result and log. */
358
+ transactionId?: string;
359
+ /** When `true`, validate + shape the result but don't persist. */
360
+ dryRun?: boolean;
361
+ /** Accepted for signature parity; not simulated. */
362
+ visibility?: "sync" | "async" | "deferred";
363
+ /** Accepted for signature parity; not simulated. */
364
+ autoGenerateArrayKeys?: boolean;
365
+ /** Accepted for signature parity; not simulated. */
366
+ tag?: string;
208
367
  }
209
368
 
210
369
  export declare interface PatchExecutor {
@@ -275,6 +434,17 @@ export declare type Perspective = "raw" | "published" | "drafts" | string[];
275
434
  /** Strip `drafts.` / `versions.<name>.` prefix to recover the published id. */
276
435
  export declare function publishedIdOf(id: string): string;
277
436
 
437
+ /**
438
+ * Shape returned by `fetch` when `filterResponse: false`. Matches
439
+ * `@sanity/client`'s `RawQueryResponse`. `query` is omitted when the
440
+ * caller also passes `returnQuery: false`.
441
+ */
442
+ export declare interface RawQueryResponse<R> {
443
+ query?: string;
444
+ ms: number;
445
+ result: R;
446
+ }
447
+
278
448
  export declare type ReleaseAction =
279
449
  | {
280
450
  actionType: "sanity.action.release.create";
@@ -400,6 +570,37 @@ declare interface SanityDocumentLike {
400
570
  [key: string]: unknown;
401
571
  }
402
572
 
573
+ /** Result of a mutation when `returnFirst` resolves true and ids are requested. */
574
+ export declare interface SingleMutationResult {
575
+ transactionId: string;
576
+ documentId: string;
577
+ results: {
578
+ id: string;
579
+ operation: MutationOperation;
580
+ }[];
581
+ }
582
+
583
+ /** Members this fake actually implements — surfaced in error messages. */
584
+ export declare const SUPPORTED_SURFACE: readonly [
585
+ "fetch",
586
+ "getDocument",
587
+ "getDocuments",
588
+ "create",
589
+ "createOrReplace",
590
+ "createIfNotExists",
591
+ "delete",
592
+ "patch",
593
+ "transaction",
594
+ "mutate",
595
+ "action",
596
+ "releases",
597
+ "createVersion",
598
+ "unpublishVersion",
599
+ "request (only with accessControl)",
600
+ "config",
601
+ "withConfig",
602
+ ];
603
+
403
604
  export declare interface SystemFields {
404
605
  _rev: string;
405
606
  _createdAt: string;
@@ -415,7 +616,7 @@ export declare interface TestClient {
415
616
  getDocument: <T = SanityDocument>(
416
617
  id: string,
417
618
  options?: FetchOptions,
418
- ) => Promise<T | null>;
619
+ ) => Promise<T | undefined>;
419
620
  getDocuments: <T = SanityDocument>(
420
621
  ids: string[],
421
622
  options?: FetchOptions,
@@ -458,6 +659,17 @@ export declare interface TestClient {
458
659
  } | null>;
459
660
  patch: (documentId: string) => PatchHandle;
460
661
  transaction: () => TransactionHandle;
662
+ /**
663
+ * Apply a batch of mutations in the raw array form (or a `patch()` /
664
+ * `transaction()` builder) — mirrors `@sanity/client`'s `mutate()`.
665
+ * Atomic: any failure rolls the whole batch back. The resolved shape
666
+ * follows `options.returnDocuments` / `returnFirst` exactly like the
667
+ * real client (default → {@link MultipleMutationResult}).
668
+ */
669
+ mutate: (
670
+ operations: FakeMutation[] | PatchHandle | TransactionHandle,
671
+ options?: TxnCommitOptions,
672
+ ) => Promise<TxnCommitResult>;
461
673
  /**
462
674
  * Dispatch one or more release / version actions against the store.
463
675
  * Returns a synthetic `transactionId`. Multi-action arrays are applied
@@ -511,6 +723,46 @@ export declare interface TestClient {
511
723
  projectId: string;
512
724
  dataset: string;
513
725
  }[];
726
+ /**
727
+ * Register a handler for a `@sanity/client` member the fake doesn't
728
+ * implement (e.g. `listen`, `assets`). Accessing that member on the
729
+ * client then returns the handler instead of throwing the
730
+ * "not implemented" guard error. Shared across sibling `withConfig`
731
+ * handles. Stubs fill gaps only — they don't shadow implemented methods.
732
+ */
733
+ stub: (name: string, handler: unknown) => void;
734
+ /** The currently-registered stubs, keyed by member name. */
735
+ stubs: () => Record<string, unknown>;
736
+ /**
737
+ * Every mutation committed through this client's registry, flattened
738
+ * and in order — `create` / `createOrReplace` / `createIfNotExists` /
739
+ * `delete` / `patch`, whether issued directly, via `transaction()`, or
740
+ * via `mutate()`. The spy hook for "assert exactly what writes my code
741
+ * submitted".
742
+ */
743
+ mutations: () => TxnMutation[];
744
+ /**
745
+ * Every committed transaction, in order. A direct mutation is recorded
746
+ * as a one-mutation transaction. Each entry carries its synthetic
747
+ * `transactionId` and the `(projectId, dataset)` it targeted.
748
+ */
749
+ transactions: () => MutationLogEntry[];
750
+ /** Clear the recorded mutation / transaction log. */
751
+ clearLog: () => void;
752
+ /**
753
+ * Every recorded method call, in order — the spy layer for "was `fetch`
754
+ * called, how many times, and with what params". Records the
755
+ * `@sanity/client`-surface methods (`fetch`, `getDocument`, `create`,
756
+ * `patch`, `mutate`, `releases.*`, …), not the test-only helpers.
757
+ * Pass a method name to filter (e.g. `calls("fetch")`); namespace
758
+ * methods are dotted (`calls("releases.create")`). Shared across
759
+ * `withConfig` siblings.
760
+ */
761
+ calls: (method?: string) => CallRecord[];
762
+ /** Number of recorded calls, optionally filtered by method name. */
763
+ callCount: (method?: string) => number;
764
+ /** Clear the recorded call log. */
765
+ clearCalls: () => void;
514
766
  }
515
767
 
516
768
  export declare interface TestClientAccessControl {
@@ -569,20 +821,22 @@ export declare interface TransactionHandle {
569
821
  }
570
822
 
571
823
  export declare interface TxnCommitOptions {
572
- /** If true, returns the resulting documents instead of just ids. */
824
+ /**
825
+ * Shape the resolved value (mirrors `@sanity/client`):
826
+ * - default (both unset) → {@link MultipleMutationResult}
827
+ * - `returnDocuments: true` → the resulting document(s)
828
+ * - `returnFirst: true` with `returnDocuments` → a single document
829
+ * - `returnDocuments: false` → an id-shaped mutation result
830
+ */
573
831
  returnDocuments?: boolean;
574
- /** With `returnDocuments`, return only the first updated doc. */
575
832
  returnFirst?: boolean;
833
+ /** Override the synthetic transaction id (mirrors `client.transaction(undefined, { transactionId })`). */
834
+ transactionId?: string;
576
835
  }
577
836
 
578
837
  export declare type TxnCommitResult =
579
- | {
580
- transactionId: string;
581
- results: {
582
- id: string;
583
- operation: "create" | "update" | "delete";
584
- }[];
585
- }
838
+ | MultipleMutationResult
839
+ | SingleMutationResult
586
840
  | SanityDocument
587
841
  | SanityDocument[];
588
842
 
@@ -624,6 +878,20 @@ export declare type TxnMutation =
624
878
  ifRev?: string;
625
879
  };
626
880
 
881
+ /**
882
+ * Curated map of real `@sanity/client` members the fake deliberately omits.
883
+ * Keyed by the top-level property name; nested access (e.g.
884
+ * `assets.upload`) is reported with the full dotted path.
885
+ */
886
+ export declare const UNSUPPORTED_MEMBERS: Record<string, UnsupportedEntry>;
887
+
888
+ declare interface UnsupportedEntry {
889
+ /** What the real member is. */
890
+ what: string;
891
+ /** What to do instead. */
892
+ workaround: string;
893
+ }
894
+
627
895
  export declare type VersionAction =
628
896
  | {
629
897
  actionType: "sanity.action.document.version.create";
@@ -645,6 +913,18 @@ export declare type VersionAction =
645
913
  publishedId: string;
646
914
  };
647
915
 
916
+ /**
917
+ * Wrap a client object so unimplemented members fail informatively,
918
+ * registered stubs are served, and recorded methods report their calls.
919
+ *
920
+ * @param client - the fully-built client object (plain literal).
921
+ * @param config - stub resolver plus optional call-recording hooks.
922
+ */
923
+ export declare function wrapWithGuards<T extends object>(
924
+ client: T,
925
+ config: GuardConfig,
926
+ ): T;
927
+
648
928
  /**
649
929
  * Sanity-style 403 raised when a configured-grants check rejects a
650
930
  * write. The thrown object carries the doc id, the attempted