@sanity/workflow-engine-test 0.1.0 → 0.2.1
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 +39 -0
- package/dist/index.cjs +5094 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +711 -6
- package/dist/index.d.ts +711 -6
- package/dist/index.js +5092 -48
- package/dist/index.js.map +1 -1
- package/package.json +6 -3
package/dist/index.d.ts
CHANGED
|
@@ -13,13 +13,17 @@ import type { OperationArgs } from "@sanity/workflow-engine";
|
|
|
13
13
|
import { PendingEffect } from "@sanity/workflow-engine";
|
|
14
14
|
import type { StartInstanceArgs } from "@sanity/workflow-engine";
|
|
15
15
|
import { TaskStatus } from "@sanity/workflow-engine";
|
|
16
|
-
import type { TestClient } from "@sanity-labs/client-fake-for-test";
|
|
17
|
-
import type { TestClientAccessControl } from "@sanity-labs/client-fake-for-test";
|
|
18
16
|
import type { WorkflowAccess } from "@sanity/workflow-engine";
|
|
19
17
|
import { WorkflowEvaluation } from "@sanity/workflow-engine";
|
|
20
18
|
import { WorkflowInstance } from "@sanity/workflow-engine";
|
|
21
19
|
import type { WorkflowResource } from "@sanity/workflow-engine";
|
|
22
20
|
|
|
21
|
+
declare type Action = ReleaseAction | VersionAction;
|
|
22
|
+
|
|
23
|
+
declare interface ActionResult {
|
|
24
|
+
transactionId: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
23
27
|
/**
|
|
24
28
|
* The bench's default access state — wildcard user + wildcard
|
|
25
29
|
* grants. The bench injects this as the `access` override on every
|
|
@@ -102,6 +106,26 @@ declare interface BenchState {
|
|
|
102
106
|
grants: Grant[];
|
|
103
107
|
/** Add documents to the store after construction. */
|
|
104
108
|
seedDocuments: (documents: StoreDocument[]) => void;
|
|
109
|
+
/**
|
|
110
|
+
* Current bench clock reading (ISO). Wall-clock until {@link Bench.setNow}
|
|
111
|
+
* or {@link Bench.advance} freezes it. Handy for computing deadlines
|
|
112
|
+
* relative to frozen time in a test.
|
|
113
|
+
*/
|
|
114
|
+
now: () => string;
|
|
115
|
+
/**
|
|
116
|
+
* Freeze the bench clock at an explicit ISO instant. Every subsequent
|
|
117
|
+
* engine call (`startInstance` / `fireAction` / `tick` / `completeEffect`
|
|
118
|
+
* / `evaluate`) sees this as `$now` and stamps it on history, so
|
|
119
|
+
* time-based `completeWhen` / `failWhen` / SLA predicates are
|
|
120
|
+
* deterministic.
|
|
121
|
+
*/
|
|
122
|
+
setNow: (iso: string) => void;
|
|
123
|
+
/**
|
|
124
|
+
* Move the bench clock forward by `ms`, freezing it at
|
|
125
|
+
* `current + ms`. Re-evaluate by calling {@link Bench.tick} — `advance`
|
|
126
|
+
* itself never writes, so the timeline only progresses when you tick.
|
|
127
|
+
*/
|
|
128
|
+
advance: (ms: number) => void;
|
|
105
129
|
}
|
|
106
130
|
|
|
107
131
|
declare type BenchTickArgs = Omit<
|
|
@@ -113,6 +137,26 @@ declare type BenchTickArgs = Omit<
|
|
|
113
137
|
actor?: Actor;
|
|
114
138
|
};
|
|
115
139
|
|
|
140
|
+
/**
|
|
141
|
+
* One recorded invocation of a client method — the spy layer behind
|
|
142
|
+
* `client.calls()` / `client.callCount()`. `result` holds the resolved
|
|
143
|
+
* value (awaited, for async methods); `error` holds the thrown/rejected
|
|
144
|
+
* value. Builder-returning methods (`patch`, `transaction`) record the
|
|
145
|
+
* builder as `result`; their eventual writes show up in the mutation log.
|
|
146
|
+
*/
|
|
147
|
+
declare interface CallRecord {
|
|
148
|
+
/** Method name; namespace methods are dotted, e.g. `releases.create`. */
|
|
149
|
+
method: string;
|
|
150
|
+
/** Arguments as passed (stored by reference, like `vi.fn`). */
|
|
151
|
+
args: unknown[];
|
|
152
|
+
/** Resolved/returned value, set once the call settles successfully. */
|
|
153
|
+
result?: unknown;
|
|
154
|
+
/** Thrown or rejected value, set if the call failed. */
|
|
155
|
+
error?: unknown;
|
|
156
|
+
projectId: string;
|
|
157
|
+
dataset: string;
|
|
158
|
+
}
|
|
159
|
+
|
|
116
160
|
/**
|
|
117
161
|
* Create a fresh bench. By default the bench is fully isolated — two
|
|
118
162
|
* `createBench()` calls have no shared state. Pass `client` + `tags`
|
|
@@ -181,11 +225,45 @@ export declare interface CreateBenchOptions {
|
|
|
181
225
|
* URIs read as `dataset:test:test:<docId>`.
|
|
182
226
|
*/
|
|
183
227
|
workflowResource?: WorkflowResource;
|
|
228
|
+
/**
|
|
229
|
+
* Freeze the bench clock at this ISO instant from construction, so
|
|
230
|
+
* `$now` and every engine timestamp are deterministic. Equivalent to
|
|
231
|
+
* calling {@link Bench.setNow} immediately after `createBench`. Omit to
|
|
232
|
+
* start on the wall clock (`advance` / `setNow` can freeze it later).
|
|
233
|
+
*/
|
|
234
|
+
now?: string;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
declare interface CurrentUser {
|
|
238
|
+
id: string;
|
|
239
|
+
name?: string;
|
|
240
|
+
email?: string;
|
|
241
|
+
/** @deprecated singular `role` on legacy responses */
|
|
242
|
+
role?: string;
|
|
243
|
+
roles?: {
|
|
244
|
+
name: string;
|
|
245
|
+
title?: string;
|
|
246
|
+
description?: string;
|
|
247
|
+
}[];
|
|
184
248
|
}
|
|
185
249
|
|
|
186
250
|
/** Default workflow resource for benches that don't pass one. */
|
|
187
251
|
export declare const DEFAULT_WORKFLOW_RESOURCE: WorkflowResource;
|
|
188
252
|
|
|
253
|
+
/**
|
|
254
|
+
* Document body accepted by `version.create` / `createVersion`. The id
|
|
255
|
+
* is derived from `releaseId + publishedId` so callers may omit `_id`
|
|
256
|
+
* here. `version.replace` requires `_id` since it identifies the doc
|
|
257
|
+
* to replace.
|
|
258
|
+
*/
|
|
259
|
+
declare interface DocumentBody {
|
|
260
|
+
_id?: string;
|
|
261
|
+
_type: string;
|
|
262
|
+
[field: string]: unknown;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
declare type DocumentValuePermission = "create" | "read" | "update";
|
|
266
|
+
|
|
189
267
|
/** Engine-call wrappers that inject the bench-owned client + tags + access. */
|
|
190
268
|
declare type EngineWrappers = {
|
|
191
269
|
deployDefinitions: (
|
|
@@ -196,8 +274,96 @@ declare type EngineWrappers = {
|
|
|
196
274
|
completeEffect: (args: BenchCompleteEffectArgs) => Promise<DispatchResult>;
|
|
197
275
|
tick: (args: BenchTickArgs) => Promise<DispatchResult>;
|
|
198
276
|
evaluate: (args: BenchEvaluateArgs) => Promise<WorkflowEvaluation>;
|
|
277
|
+
guardsForInstance: (instanceId: string) => Promise<MutationGuardDoc[]>;
|
|
199
278
|
};
|
|
200
279
|
|
|
280
|
+
/**
|
|
281
|
+
* A single mutation in the raw array form accepted by `mutate()` — the
|
|
282
|
+
* same shape `@sanity/client` accepts. The inline `patch` form mirrors
|
|
283
|
+
* the real client's keyed patch object.
|
|
284
|
+
*/
|
|
285
|
+
declare type FakeMutation =
|
|
286
|
+
| {
|
|
287
|
+
create: SanityDocumentLike;
|
|
288
|
+
}
|
|
289
|
+
| {
|
|
290
|
+
createOrReplace: SanityDocumentLike;
|
|
291
|
+
}
|
|
292
|
+
| {
|
|
293
|
+
createIfNotExists: SanityDocumentLike;
|
|
294
|
+
}
|
|
295
|
+
| {
|
|
296
|
+
delete: {
|
|
297
|
+
id: string;
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
| {
|
|
301
|
+
patch: {
|
|
302
|
+
id: string;
|
|
303
|
+
ifRevisionID?: string;
|
|
304
|
+
set?: Record<string, unknown>;
|
|
305
|
+
setIfMissing?: Record<string, unknown>;
|
|
306
|
+
unset?: string[];
|
|
307
|
+
inc?: Record<string, number>;
|
|
308
|
+
dec?: Record<string, number>;
|
|
309
|
+
insert?: {
|
|
310
|
+
before?: string;
|
|
311
|
+
after?: string;
|
|
312
|
+
replace?: string;
|
|
313
|
+
items: unknown[];
|
|
314
|
+
};
|
|
315
|
+
};
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Data-plane subset of `@sanity/client`'s surface, with multi-dataset support.
|
|
320
|
+
*
|
|
321
|
+
* One `createTestClient()` call creates a registry of stores keyed by
|
|
322
|
+
* `(projectId, dataset)`. The returned client points at one of them.
|
|
323
|
+
* `withConfig({ projectId, dataset })` returns a new handle into the same
|
|
324
|
+
* registry but pointing at a different store — useful for workflows where
|
|
325
|
+
* definitions live in one dataset and instances live in another.
|
|
326
|
+
*
|
|
327
|
+
* Two separate `createTestClient()` calls have separate registries, so
|
|
328
|
+
* tests stay isolated from each other.
|
|
329
|
+
*
|
|
330
|
+
* Methods not on the real client:
|
|
331
|
+
*
|
|
332
|
+
* - `seed`, `add`, `reset`, `snapshot`, `store` — bulk-load helpers,
|
|
333
|
+
* scoped to the current dataset.
|
|
334
|
+
* - `simulateConcurrentEdit()` — force the next N revision-guarded writes
|
|
335
|
+
* to a document to 409, for optimistic-locking retry tests.
|
|
336
|
+
* - `datasets()` — lists known dataset keys in this registry.
|
|
337
|
+
*
|
|
338
|
+
* Methods deliberately omitted:
|
|
339
|
+
*
|
|
340
|
+
* - Project / agent / asset / live / listen APIs.
|
|
341
|
+
*/
|
|
342
|
+
declare interface FetchOptions {
|
|
343
|
+
/** Override the client's configured perspective for this read. */
|
|
344
|
+
perspective?: Perspective;
|
|
345
|
+
/**
|
|
346
|
+
* When `false`, resolve to a {@link RawQueryResponse} (`{ query, ms,
|
|
347
|
+
* result }`) instead of the bare result — mirrors `@sanity/client`.
|
|
348
|
+
* Defaults to `true`.
|
|
349
|
+
*/
|
|
350
|
+
filterResponse?: boolean;
|
|
351
|
+
/**
|
|
352
|
+
* With `filterResponse: false`, set to `false` to omit `query` from the
|
|
353
|
+
* raw response. Ignored when `filterResponse` is not `false`.
|
|
354
|
+
*/
|
|
355
|
+
returnQuery?: boolean;
|
|
356
|
+
/** Accepted for signature parity; not simulated. */
|
|
357
|
+
tag?: string;
|
|
358
|
+
/** Accepted for signature parity; not simulated. */
|
|
359
|
+
signal?: AbortSignal;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
declare interface Grant_2 {
|
|
363
|
+
filter: string;
|
|
364
|
+
permissions: DocumentValuePermission[];
|
|
365
|
+
}
|
|
366
|
+
|
|
201
367
|
declare type GuardHelpers = {
|
|
202
368
|
/** All deployed `temp.system.guard` docs in the store. */
|
|
203
369
|
listGuards: () => Promise<MutationGuardDoc[]>;
|
|
@@ -211,10 +377,11 @@ declare type GuardHelpers = {
|
|
|
211
377
|
as?: Actor,
|
|
212
378
|
) => Promise<MutationGuardDoc[]>;
|
|
213
379
|
/**
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
380
|
+
* Write to a document through the client's enforced write seam — the bench
|
|
381
|
+
* client gates the write against deployed guards (incl. `identity()`) and
|
|
382
|
+
* throws `MutationGuardDeniedError` on denial, exactly as the lake would.
|
|
383
|
+
* `as` routes the write through a token-scoped sibling so `identity()`
|
|
384
|
+
* resolves to that actor.
|
|
218
385
|
*/
|
|
219
386
|
editDocument: (
|
|
220
387
|
documentId: string,
|
|
@@ -226,6 +393,110 @@ declare type GuardHelpers = {
|
|
|
226
393
|
) => Promise<StoreDocument>;
|
|
227
394
|
};
|
|
228
395
|
|
|
396
|
+
/** Result of a mutation when ids are requested for all affected documents. */
|
|
397
|
+
declare interface MultipleMutationResult {
|
|
398
|
+
transactionId: string;
|
|
399
|
+
documentIds: string[];
|
|
400
|
+
results: {
|
|
401
|
+
id: string;
|
|
402
|
+
operation: MutationOperation;
|
|
403
|
+
}[];
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** Append-only record of one committed transaction (a single mutation is a 1-entry transaction). */
|
|
407
|
+
declare interface MutationLogEntry {
|
|
408
|
+
transactionId: string;
|
|
409
|
+
projectId: string;
|
|
410
|
+
dataset: string;
|
|
411
|
+
mutations: TxnMutation[];
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** The operation recorded per affected document, matching `@sanity/client`. */
|
|
415
|
+
declare type MutationOperation = "create" | "update" | "delete" | "none";
|
|
416
|
+
|
|
417
|
+
declare interface MutationOptions {
|
|
418
|
+
ifRevisionId?: string;
|
|
419
|
+
/**
|
|
420
|
+
* Shape the resolved value, mirroring `@sanity/client`:
|
|
421
|
+
* - default → the mutated document
|
|
422
|
+
* - `returnDocuments: false` → an id-shaped mutation result
|
|
423
|
+
* ({@link SingleMutationResult} / {@link MultipleMutationResult})
|
|
424
|
+
* - `returnFirst: false` with documents → an array
|
|
425
|
+
*
|
|
426
|
+
* The static return type reflects the default (the document); pass a
|
|
427
|
+
* type argument or cast when requesting an id-shaped result.
|
|
428
|
+
*/
|
|
429
|
+
returnDocuments?: boolean;
|
|
430
|
+
returnFirst?: boolean;
|
|
431
|
+
/** Override the synthetic transaction id used in the result and log. */
|
|
432
|
+
transactionId?: string;
|
|
433
|
+
/** When `true`, validate + shape the result but don't persist. */
|
|
434
|
+
dryRun?: boolean;
|
|
435
|
+
/** Accepted for signature parity; not simulated. */
|
|
436
|
+
visibility?: "sync" | "async" | "deferred";
|
|
437
|
+
/** Accepted for signature parity; not simulated. */
|
|
438
|
+
autoGenerateArrayKeys?: boolean;
|
|
439
|
+
/** Accepted for signature parity; not simulated. */
|
|
440
|
+
tag?: string;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
declare interface PatchHandle {
|
|
444
|
+
set: (props: Record<string, unknown>) => PatchHandle;
|
|
445
|
+
setIfMissing: (props: Record<string, unknown>) => PatchHandle;
|
|
446
|
+
unset: (paths: string[]) => PatchHandle;
|
|
447
|
+
inc: (props: Record<string, number>) => PatchHandle;
|
|
448
|
+
dec: (props: Record<string, number>) => PatchHandle;
|
|
449
|
+
insert: (
|
|
450
|
+
position: "before" | "after" | "replace",
|
|
451
|
+
anchor: string,
|
|
452
|
+
items: unknown[],
|
|
453
|
+
) => PatchHandle;
|
|
454
|
+
ifRevisionId: (rev: string) => PatchHandle;
|
|
455
|
+
commit: () => Promise<SanityDocument>;
|
|
456
|
+
/** Test-only introspection — not on real Patch */
|
|
457
|
+
toJSON: () => {
|
|
458
|
+
id: string;
|
|
459
|
+
ops: PatchOp[];
|
|
460
|
+
ifRev?: string;
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Patch operations the test client supports.
|
|
466
|
+
*
|
|
467
|
+
* Subset of `@sanity/client`'s PatchOperations. Each entry is one
|
|
468
|
+
* mutation step applied in order at commit time.
|
|
469
|
+
*/
|
|
470
|
+
declare type PatchOp =
|
|
471
|
+
| {
|
|
472
|
+
kind: "set";
|
|
473
|
+
props: Record<string, unknown>;
|
|
474
|
+
}
|
|
475
|
+
| {
|
|
476
|
+
kind: "setIfMissing";
|
|
477
|
+
props: Record<string, unknown>;
|
|
478
|
+
}
|
|
479
|
+
| {
|
|
480
|
+
kind: "unset";
|
|
481
|
+
paths: string[];
|
|
482
|
+
}
|
|
483
|
+
| {
|
|
484
|
+
kind: "inc";
|
|
485
|
+
props: Record<string, number>;
|
|
486
|
+
}
|
|
487
|
+
| {
|
|
488
|
+
kind: "dec";
|
|
489
|
+
props: Record<string, number>;
|
|
490
|
+
}
|
|
491
|
+
| {
|
|
492
|
+
kind: "insert";
|
|
493
|
+
position: "before" | "after" | "replace";
|
|
494
|
+
anchor: string;
|
|
495
|
+
items: unknown[];
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
declare type Perspective = "raw" | "published" | "drafts" | string[];
|
|
499
|
+
|
|
229
500
|
declare type QueryHelpers = {
|
|
230
501
|
/**
|
|
231
502
|
* Run an arbitrary GROQ query through `workflow.query`. The query MUST
|
|
@@ -310,6 +581,134 @@ declare type ReadHelpers = {
|
|
|
310
581
|
snapshot: () => readonly StoreDocument[];
|
|
311
582
|
};
|
|
312
583
|
|
|
584
|
+
declare type ReleaseAction =
|
|
585
|
+
| {
|
|
586
|
+
actionType: "sanity.action.release.create";
|
|
587
|
+
releaseId: string;
|
|
588
|
+
metadata?: Partial<ReleaseMetadata>;
|
|
589
|
+
}
|
|
590
|
+
| {
|
|
591
|
+
actionType: "sanity.action.release.edit";
|
|
592
|
+
releaseId: string;
|
|
593
|
+
patch: {
|
|
594
|
+
set?: Record<string, unknown>;
|
|
595
|
+
unset?: string[];
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
| {
|
|
599
|
+
actionType: "sanity.action.release.schedule";
|
|
600
|
+
releaseId: string;
|
|
601
|
+
publishAt: string;
|
|
602
|
+
}
|
|
603
|
+
| {
|
|
604
|
+
actionType: "sanity.action.release.unschedule";
|
|
605
|
+
releaseId: string;
|
|
606
|
+
}
|
|
607
|
+
| {
|
|
608
|
+
actionType: "sanity.action.release.publish";
|
|
609
|
+
releaseId: string;
|
|
610
|
+
}
|
|
611
|
+
| {
|
|
612
|
+
actionType: "sanity.action.release.archive";
|
|
613
|
+
releaseId: string;
|
|
614
|
+
}
|
|
615
|
+
| {
|
|
616
|
+
actionType: "sanity.action.release.unarchive";
|
|
617
|
+
releaseId: string;
|
|
618
|
+
}
|
|
619
|
+
| {
|
|
620
|
+
actionType: "sanity.action.release.delete";
|
|
621
|
+
releaseId: string;
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
declare interface ReleaseDocument extends SanityDocument {
|
|
625
|
+
_type: "system.release";
|
|
626
|
+
name: string;
|
|
627
|
+
state: ReleaseState;
|
|
628
|
+
metadata: ReleaseMetadata;
|
|
629
|
+
publishAt?: string;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
declare interface ReleaseMetadata {
|
|
633
|
+
title?: string;
|
|
634
|
+
description?: string;
|
|
635
|
+
releaseType?: ReleaseType;
|
|
636
|
+
intendedPublishAt?: string;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
declare interface ReleasesHandle {
|
|
640
|
+
create: (params: {
|
|
641
|
+
releaseId: string;
|
|
642
|
+
metadata?: Partial<ReleaseMetadata>;
|
|
643
|
+
}) => Promise<
|
|
644
|
+
ActionResult & {
|
|
645
|
+
releaseId: string;
|
|
646
|
+
metadata: ReleaseMetadata;
|
|
647
|
+
}
|
|
648
|
+
>;
|
|
649
|
+
edit: (params: {
|
|
650
|
+
releaseId: string;
|
|
651
|
+
patch: {
|
|
652
|
+
set?: Record<string, unknown>;
|
|
653
|
+
unset?: string[];
|
|
654
|
+
};
|
|
655
|
+
}) => Promise<ActionResult>;
|
|
656
|
+
get: (params: { releaseId: string }) => Promise<ReleaseDocument | undefined>;
|
|
657
|
+
fetchDocuments: (params: { releaseId: string }) => Promise<SanityDocument[]>;
|
|
658
|
+
schedule: (params: {
|
|
659
|
+
releaseId: string;
|
|
660
|
+
publishAt: string;
|
|
661
|
+
}) => Promise<ActionResult>;
|
|
662
|
+
unschedule: (params: { releaseId: string }) => Promise<ActionResult>;
|
|
663
|
+
publish: (params: { releaseId: string }) => Promise<ActionResult>;
|
|
664
|
+
archive: (params: { releaseId: string }) => Promise<ActionResult>;
|
|
665
|
+
unarchive: (params: { releaseId: string }) => Promise<ActionResult>;
|
|
666
|
+
delete: (params: { releaseId: string }) => Promise<ActionResult>;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
declare type ReleaseState = "active" | "scheduled" | "published" | "archived";
|
|
670
|
+
|
|
671
|
+
declare type ReleaseType = "asap" | "scheduled" | "undecided";
|
|
672
|
+
|
|
673
|
+
/** @public */
|
|
674
|
+
declare interface SanityDocument {
|
|
675
|
+
_id: string;
|
|
676
|
+
_type: string;
|
|
677
|
+
_createdAt: string;
|
|
678
|
+
_updatedAt: string;
|
|
679
|
+
_rev: string;
|
|
680
|
+
[key: string]: unknown;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Similar to `SanityDocument` but only requires the `_id` and `_type`
|
|
685
|
+
*
|
|
686
|
+
* @see SanityDocument
|
|
687
|
+
*
|
|
688
|
+
* @public
|
|
689
|
+
*/
|
|
690
|
+
declare interface SanityDocumentLike {
|
|
691
|
+
_id: string;
|
|
692
|
+
_type: string;
|
|
693
|
+
_createdAt?: string;
|
|
694
|
+
_updatedAt?: string;
|
|
695
|
+
_rev?: string;
|
|
696
|
+
_system?: {
|
|
697
|
+
delete?: boolean;
|
|
698
|
+
};
|
|
699
|
+
[key: string]: unknown;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/** Result of a mutation when `returnFirst` resolves true and ids are requested. */
|
|
703
|
+
declare interface SingleMutationResult {
|
|
704
|
+
transactionId: string;
|
|
705
|
+
documentId: string;
|
|
706
|
+
results: {
|
|
707
|
+
id: string;
|
|
708
|
+
operation: MutationOperation;
|
|
709
|
+
}[];
|
|
710
|
+
}
|
|
711
|
+
|
|
313
712
|
/** A document as stored in the bench's fake lake. */
|
|
314
713
|
declare type StoreDocument = {
|
|
315
714
|
_id: string;
|
|
@@ -317,6 +716,312 @@ declare type StoreDocument = {
|
|
|
317
716
|
[key: string]: unknown;
|
|
318
717
|
};
|
|
319
718
|
|
|
719
|
+
declare interface SystemFields {
|
|
720
|
+
_rev: string;
|
|
721
|
+
_createdAt: string;
|
|
722
|
+
_updatedAt: string;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
declare interface TestClient {
|
|
726
|
+
fetch: <T = unknown>(
|
|
727
|
+
query: string,
|
|
728
|
+
params?: Record<string, unknown>,
|
|
729
|
+
options?: FetchOptions,
|
|
730
|
+
) => Promise<T>;
|
|
731
|
+
getDocument: <T = SanityDocument>(
|
|
732
|
+
id: string,
|
|
733
|
+
options?: FetchOptions,
|
|
734
|
+
) => Promise<T | undefined>;
|
|
735
|
+
getDocuments: <T = SanityDocument>(
|
|
736
|
+
ids: string[],
|
|
737
|
+
options?: FetchOptions,
|
|
738
|
+
) => Promise<(T | null)[]>;
|
|
739
|
+
/**
|
|
740
|
+
* Sanity-style HTTP request hook. Present iff the client was
|
|
741
|
+
* created with `accessControl`. The engine's access resolver pokes
|
|
742
|
+
* `/users/me` and the configured ACL path through this — without it
|
|
743
|
+
* the engine throws on any verb that needs an actor (which is the
|
|
744
|
+
* cue for the bench to supply an `access` override).
|
|
745
|
+
*
|
|
746
|
+
* Test cases that want to exercise the engine's token-resolution
|
|
747
|
+
* path should configure `accessControl`; cases that want bench-style
|
|
748
|
+
* always-allow should leave it off and pass `access` through the
|
|
749
|
+
* bench.
|
|
750
|
+
*/
|
|
751
|
+
request?: <T>(opts: {
|
|
752
|
+
url?: string;
|
|
753
|
+
uri?: string;
|
|
754
|
+
signal?: AbortSignal;
|
|
755
|
+
tag?: string;
|
|
756
|
+
}) => Promise<T>;
|
|
757
|
+
create: <T extends SanityDocumentLike>(
|
|
758
|
+
doc: T,
|
|
759
|
+
options?: MutationOptions,
|
|
760
|
+
) => Promise<T & SystemFields>;
|
|
761
|
+
createOrReplace: <T extends SanityDocumentLike>(
|
|
762
|
+
doc: T,
|
|
763
|
+
options?: MutationOptions,
|
|
764
|
+
) => Promise<T & SystemFields>;
|
|
765
|
+
createIfNotExists: <T extends SanityDocumentLike>(
|
|
766
|
+
doc: T,
|
|
767
|
+
options?: MutationOptions,
|
|
768
|
+
) => Promise<T & SystemFields>;
|
|
769
|
+
delete: (
|
|
770
|
+
id: string,
|
|
771
|
+
options?: MutationOptions,
|
|
772
|
+
) => Promise<{
|
|
773
|
+
_id: string;
|
|
774
|
+
} | null>;
|
|
775
|
+
patch: (documentId: string) => PatchHandle;
|
|
776
|
+
transaction: () => TransactionHandle;
|
|
777
|
+
/**
|
|
778
|
+
* Apply a batch of mutations in the raw array form (or a `patch()` /
|
|
779
|
+
* `transaction()` builder) — mirrors `@sanity/client`'s `mutate()`.
|
|
780
|
+
* Atomic: any failure rolls the whole batch back. The resolved shape
|
|
781
|
+
* follows `options.returnDocuments` / `returnFirst` exactly like the
|
|
782
|
+
* real client (default → {@link MultipleMutationResult}).
|
|
783
|
+
*/
|
|
784
|
+
mutate: (
|
|
785
|
+
operations: FakeMutation[] | PatchHandle | TransactionHandle,
|
|
786
|
+
options?: TxnCommitOptions,
|
|
787
|
+
) => Promise<TxnCommitResult>;
|
|
788
|
+
/**
|
|
789
|
+
* Dispatch one or more release / version actions against the store.
|
|
790
|
+
* Returns a synthetic `transactionId`. Multi-action arrays are applied
|
|
791
|
+
* atomically — any failure rolls back the batch.
|
|
792
|
+
*/
|
|
793
|
+
action: (action: Action | Action[]) => Promise<ActionResult>;
|
|
794
|
+
/** Helpers mirroring `@sanity/client`'s `client.releases.*` surface. */
|
|
795
|
+
releases: ReleasesHandle;
|
|
796
|
+
/** Helper for `sanity.action.document.version.create`. */
|
|
797
|
+
createVersion: (params: {
|
|
798
|
+
releaseId: string;
|
|
799
|
+
publishedId: string;
|
|
800
|
+
document: DocumentBody;
|
|
801
|
+
}) => Promise<ActionResult>;
|
|
802
|
+
/** Helper for `sanity.action.document.version.unpublish`. */
|
|
803
|
+
unpublishVersion: (params: {
|
|
804
|
+
releaseId: string;
|
|
805
|
+
publishedId: string;
|
|
806
|
+
}) => Promise<ActionResult>;
|
|
807
|
+
/** Current `(projectId, dataset)` and active perspective. */
|
|
808
|
+
config: () => {
|
|
809
|
+
projectId: string;
|
|
810
|
+
dataset: string;
|
|
811
|
+
perspective: Perspective;
|
|
812
|
+
};
|
|
813
|
+
/**
|
|
814
|
+
* Return a new client handle pointing at a different `(projectId, dataset)`
|
|
815
|
+
* within the SAME backing registry. Either field can be omitted to keep
|
|
816
|
+
* the current value. Two clients produced by `withConfig` see each
|
|
817
|
+
* other's writes if they point at the same `(projectId, dataset)`.
|
|
818
|
+
*/
|
|
819
|
+
withConfig: (
|
|
820
|
+
config: Partial<{
|
|
821
|
+
projectId: string;
|
|
822
|
+
dataset: string;
|
|
823
|
+
perspective: Perspective;
|
|
824
|
+
/**
|
|
825
|
+
* Rebind the acting identity (and its grants) on a sibling that shares
|
|
826
|
+
* the same store — the test analogue of a token-scoped client. Lets two
|
|
827
|
+
* actors write the same dataset, e.g. for `identity()`-based guards.
|
|
828
|
+
*/
|
|
829
|
+
accessControl: TestClientAccessControl;
|
|
830
|
+
}>,
|
|
831
|
+
) => TestClient;
|
|
832
|
+
/** Replace the current dataset with the given documents. */
|
|
833
|
+
seed: (documents: SanityDocumentLike[]) => void;
|
|
834
|
+
/** Add documents to the current dataset without dropping existing ones. */
|
|
835
|
+
add: (documents: SanityDocumentLike[]) => void;
|
|
836
|
+
/** Drop all documents in the current dataset. */
|
|
837
|
+
reset: () => void;
|
|
838
|
+
/** Read-only snapshot of all documents in the current dataset. */
|
|
839
|
+
snapshot: () => readonly SanityDocument[];
|
|
840
|
+
/** Direct store access for the current dataset. */
|
|
841
|
+
store: () => TestStore;
|
|
842
|
+
/**
|
|
843
|
+
* Simulate `count` concurrent edits to `id` in the current dataset: the
|
|
844
|
+
* next `count` revision-guarded mutations (those passing `ifRevisionId`)
|
|
845
|
+
* to that document throw a 409-shaped conflict, as if another client
|
|
846
|
+
* committed between the caller's read and write. Lets a test exercise
|
|
847
|
+
* optimistic-locking retry paths deterministically, without timing or
|
|
848
|
+
* interleaving. Mutations that don't pass `ifRevisionId` are unaffected.
|
|
849
|
+
*/
|
|
850
|
+
simulateConcurrentEdit: (id: string, count?: number) => void;
|
|
851
|
+
/** List `(projectId, dataset)` pairs known in this registry. */
|
|
852
|
+
datasets: () => {
|
|
853
|
+
projectId: string;
|
|
854
|
+
dataset: string;
|
|
855
|
+
}[];
|
|
856
|
+
/**
|
|
857
|
+
* Register a handler for a `@sanity/client` member the fake doesn't
|
|
858
|
+
* implement (e.g. `listen`, `assets`). Accessing that member on the
|
|
859
|
+
* client then returns the handler instead of throwing the
|
|
860
|
+
* "not implemented" guard error. Shared across sibling `withConfig`
|
|
861
|
+
* handles. Stubs fill gaps only — they don't shadow implemented methods.
|
|
862
|
+
*/
|
|
863
|
+
stub: (name: string, handler: unknown) => void;
|
|
864
|
+
/** The currently-registered stubs, keyed by member name. */
|
|
865
|
+
stubs: () => Record<string, unknown>;
|
|
866
|
+
/**
|
|
867
|
+
* Every mutation committed through this client's registry, flattened
|
|
868
|
+
* and in order — `create` / `createOrReplace` / `createIfNotExists` /
|
|
869
|
+
* `delete` / `patch`, whether issued directly, via `transaction()`, or
|
|
870
|
+
* via `mutate()`. The spy hook for "assert exactly what writes my code
|
|
871
|
+
* submitted".
|
|
872
|
+
*/
|
|
873
|
+
mutations: () => TxnMutation[];
|
|
874
|
+
/**
|
|
875
|
+
* Every committed transaction, in order. A direct mutation is recorded
|
|
876
|
+
* as a one-mutation transaction. Each entry carries its synthetic
|
|
877
|
+
* `transactionId` and the `(projectId, dataset)` it targeted.
|
|
878
|
+
*/
|
|
879
|
+
transactions: () => MutationLogEntry[];
|
|
880
|
+
/** Clear the recorded mutation / transaction log. */
|
|
881
|
+
clearLog: () => void;
|
|
882
|
+
/**
|
|
883
|
+
* Every recorded method call, in order — the spy layer for "was `fetch`
|
|
884
|
+
* called, how many times, and with what params". Records the
|
|
885
|
+
* `@sanity/client`-surface methods (`fetch`, `getDocument`, `create`,
|
|
886
|
+
* `patch`, `mutate`, `releases.*`, …), not the test-only helpers.
|
|
887
|
+
* Pass a method name to filter (e.g. `calls("fetch")`); namespace
|
|
888
|
+
* methods are dotted (`calls("releases.create")`). Shared across
|
|
889
|
+
* `withConfig` siblings.
|
|
890
|
+
*/
|
|
891
|
+
calls: (method?: string) => CallRecord[];
|
|
892
|
+
/** Number of recorded calls, optionally filtered by method name. */
|
|
893
|
+
callCount: (method?: string) => number;
|
|
894
|
+
/** Clear the recorded call log. */
|
|
895
|
+
clearCalls: () => void;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
declare interface TestClientAccessControl {
|
|
899
|
+
/** What `client.request({ url: "/users/me" })` returns. */
|
|
900
|
+
currentUser: CurrentUser;
|
|
901
|
+
/** What `client.request({ url: aclPath })` returns. */
|
|
902
|
+
grants: Grant_2[];
|
|
903
|
+
/**
|
|
904
|
+
* URL path the engine's access resolver hits to fetch grants.
|
|
905
|
+
* Defaults to `/projects/<projectId>/datasets/<dataset>/acl`.
|
|
906
|
+
*/
|
|
907
|
+
aclPath?: string;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/**
|
|
911
|
+
* In-memory document store backing a TestClient. Closure-scoped — created
|
|
912
|
+
* fresh per test, no global state.
|
|
913
|
+
*
|
|
914
|
+
* The shape mirrors what `groq-js` evaluates against: an array of
|
|
915
|
+
* documents. Internally we keep a Map by `_id` for O(1) lookup, and
|
|
916
|
+
* project to an array on each query.
|
|
917
|
+
*/
|
|
918
|
+
declare interface TestStore {
|
|
919
|
+
/** Replace the entire dataset with the given documents. Existing docs are dropped. */
|
|
920
|
+
seed: (documents: SanityDocumentLike[]) => void;
|
|
921
|
+
/** Add documents without dropping existing ones. Conflicts (same _id) overwrite. */
|
|
922
|
+
add: (documents: SanityDocumentLike[]) => void;
|
|
923
|
+
/** Remove all documents. */
|
|
924
|
+
reset: () => void;
|
|
925
|
+
/** All documents as an array, in insertion order. */
|
|
926
|
+
all: () => SanityDocument[];
|
|
927
|
+
/** Get one document by _id, or undefined. */
|
|
928
|
+
get: (id: string) => SanityDocument | undefined;
|
|
929
|
+
/** Insert a fully-formed document by _id. Replaces if it already exists. */
|
|
930
|
+
put: (doc: SanityDocument) => void;
|
|
931
|
+
/** Remove a document by _id. Returns the removed doc or undefined. */
|
|
932
|
+
remove: (id: string) => SanityDocument | undefined;
|
|
933
|
+
/** Snapshot of all docs, useful for assertions. */
|
|
934
|
+
snapshot: () => readonly SanityDocument[];
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
declare interface TransactionHandle {
|
|
938
|
+
create: (doc: SanityDocumentLike) => TransactionHandle;
|
|
939
|
+
createOrReplace: (doc: SanityDocumentLike) => TransactionHandle;
|
|
940
|
+
createIfNotExists: (doc: SanityDocumentLike) => TransactionHandle;
|
|
941
|
+
delete: (id: string) => TransactionHandle;
|
|
942
|
+
/** Accept either a Patch builder (drained at commit) or an inline doc id + ops. */
|
|
943
|
+
patch: (
|
|
944
|
+
documentIdOrPatch: string | PatchHandle,
|
|
945
|
+
ops?: PatchOp[],
|
|
946
|
+
ifRev?: string,
|
|
947
|
+
) => TransactionHandle;
|
|
948
|
+
commit: (options?: TxnCommitOptions) => Promise<TxnCommitResult>;
|
|
949
|
+
/** Test-only introspection — not on real Transaction. */
|
|
950
|
+
toJSON: () => TxnMutation[];
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
declare interface TxnCommitOptions {
|
|
954
|
+
/**
|
|
955
|
+
* Shape the resolved value (mirrors `@sanity/client`):
|
|
956
|
+
* - default (both unset) → {@link MultipleMutationResult}
|
|
957
|
+
* - `returnDocuments: true` → the resulting document(s)
|
|
958
|
+
* - `returnFirst: true` with `returnDocuments` → a single document
|
|
959
|
+
* - `returnDocuments: false` → an id-shaped mutation result
|
|
960
|
+
*/
|
|
961
|
+
returnDocuments?: boolean;
|
|
962
|
+
returnFirst?: boolean;
|
|
963
|
+
/** Override the synthetic transaction id (mirrors `client.transaction(undefined, { transactionId })`). */
|
|
964
|
+
transactionId?: string;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
declare type TxnCommitResult =
|
|
968
|
+
| MultipleMutationResult
|
|
969
|
+
| SingleMutationResult
|
|
970
|
+
| SanityDocument
|
|
971
|
+
| SanityDocument[];
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* A transaction collects mutations and applies them atomically at commit
|
|
975
|
+
* time. Mutations are applied in declaration order; if any one fails, the
|
|
976
|
+
* whole transaction throws and no writes land in the store.
|
|
977
|
+
*
|
|
978
|
+
* Subset of `@sanity/client`'s Transaction. Same shape, narrower surface.
|
|
979
|
+
*/
|
|
980
|
+
declare type TxnMutation =
|
|
981
|
+
| {
|
|
982
|
+
kind: "create";
|
|
983
|
+
doc: SanityDocumentLike;
|
|
984
|
+
}
|
|
985
|
+
| {
|
|
986
|
+
kind: "createOrReplace";
|
|
987
|
+
doc: SanityDocumentLike;
|
|
988
|
+
}
|
|
989
|
+
| {
|
|
990
|
+
kind: "createIfNotExists";
|
|
991
|
+
doc: SanityDocumentLike;
|
|
992
|
+
}
|
|
993
|
+
| {
|
|
994
|
+
kind: "delete";
|
|
995
|
+
id: string;
|
|
996
|
+
}
|
|
997
|
+
| {
|
|
998
|
+
kind: "patch";
|
|
999
|
+
documentId: string;
|
|
1000
|
+
ops: PatchOp[];
|
|
1001
|
+
ifRev?: string;
|
|
1002
|
+
};
|
|
1003
|
+
|
|
1004
|
+
declare type VersionAction =
|
|
1005
|
+
| {
|
|
1006
|
+
actionType: "sanity.action.document.version.create";
|
|
1007
|
+
publishedId: string;
|
|
1008
|
+
document: DocumentBody;
|
|
1009
|
+
releaseId?: string;
|
|
1010
|
+
}
|
|
1011
|
+
| {
|
|
1012
|
+
actionType: "sanity.action.document.version.replace";
|
|
1013
|
+
document: SanityDocumentLike;
|
|
1014
|
+
}
|
|
1015
|
+
| {
|
|
1016
|
+
actionType: "sanity.action.document.version.discard";
|
|
1017
|
+
versionId: string;
|
|
1018
|
+
}
|
|
1019
|
+
| {
|
|
1020
|
+
actionType: "sanity.action.document.version.unpublish";
|
|
1021
|
+
versionId: string;
|
|
1022
|
+
publishedId: string;
|
|
1023
|
+
};
|
|
1024
|
+
|
|
320
1025
|
/**
|
|
321
1026
|
* The default grants used when tests don't specify any. A single
|
|
322
1027
|
* permissive grant matches every document and confers all permissions.
|