@sanity-labs/client-fake-for-test 0.11.0 → 0.12.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
@@ -152,12 +152,12 @@ export declare interface CreateTestClientOptions {
152
152
  */
153
153
  token?: string;
154
154
  /**
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}.
155
+ * Pluggable write interceptor — runs once for each document's net change in
156
+ * a commit. Document publish/unpublish actions expose their underlying
157
+ * draft/published mutations; release/version actions remain unhooked. Return
158
+ * denial descriptors to reject atomically with `GuardDeniedError`; a
159
+ * thrown callback error propagates unchanged. The client owns no policy.
160
+ * Inherited by `withConfig` siblings. See {@link MutationGuard}.
161
161
  */
162
162
  mutationGuard?: MutationGuard;
163
163
  /**
@@ -246,7 +246,11 @@ export declare interface DocumentBody {
246
246
  [field: string]: unknown;
247
247
  }
248
248
 
249
- export declare type DocumentValuePermission = "create" | "read" | "update";
249
+ export declare type DocumentValuePermission =
250
+ | "create"
251
+ | "manage"
252
+ | "read"
253
+ | "update";
250
254
 
251
255
  /**
252
256
  * Error helpers shared across the fake.
@@ -397,6 +401,30 @@ export declare interface GuardConfig {
397
401
  recorded?: Set<string>;
398
402
  }
399
403
 
404
+ /** HTTP-shaped guard rejection matching the error exposed by `@sanity/client`. */
405
+ export declare class GuardDeniedError extends Error {
406
+ readonly statusCode = 403;
407
+ readonly type = "guardDeniedError";
408
+ readonly denials: MutationGuardDenial[];
409
+ readonly details: GuardDeniedResponse["body"]["error"];
410
+ readonly response: GuardDeniedResponse;
411
+ constructor(denials: readonly MutationGuardDenial[]);
412
+ }
413
+
414
+ export declare interface GuardDeniedResponse {
415
+ body: {
416
+ error: {
417
+ type: "guardDeniedError";
418
+ denials: MutationGuardDenial[];
419
+ };
420
+ };
421
+ url: string;
422
+ method: "POST";
423
+ headers: Record<string, string>;
424
+ statusCode: 403;
425
+ statusMessage: "Forbidden";
426
+ }
427
+
400
428
  export declare const isDraftId: (id: string) => boolean;
401
429
 
402
430
  export declare const isReleaseDocId: (id: string) => boolean;
@@ -416,67 +444,46 @@ export declare interface MultipleMutationResult {
416
444
  }
417
445
 
418
446
  /**
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.
447
+ * Evaluates one net document change and returns every denying guard. Returning
448
+ * `void` allows observe-only hooks. A thrown value is a callback failure and
449
+ * propagates unchanged; only returned entries become {@link GuardDeniedError}.
435
450
  */
436
451
  export declare type MutationGuard = (
437
452
  context: MutationGuardContext,
438
- ) => void | Promise<void>;
453
+ ) =>
454
+ | readonly MutationGuardDenial[]
455
+ | void
456
+ | Promise<readonly MutationGuardDenial[] | void>;
439
457
 
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";
458
+ /** The Content Lake mutation actions that a {@link MutationGuard} can match. */
459
+ export declare type MutationGuardAction = "create" | "update" | "delete";
447
460
 
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
- */
461
+ /** The before/final-after image of one net document change in a commit. */
463
462
  export declare interface MutationGuardContext {
464
463
  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;
464
+ /** Original live store image. Treat it as read-only; it is not a copy. */
465
+ before: Readonly<
466
+ SanityDocumentLike & {
467
+ _id: string;
468
+ _type: string;
469
+ }
470
+ > | null;
471
+ /** Final live store image. Treat it as read-only; it is not a copy. */
472
+ after: Readonly<
473
+ SanityDocumentLike & {
474
+ _id: string;
475
+ _type: string;
476
+ }
477
+ > | null;
478
+ /** Acting project identity, or `undefined` on an identity-free client. */
477
479
  identity: string | undefined;
478
480
  }
479
481
 
482
+ export declare interface MutationGuardDenial {
483
+ guardID: string;
484
+ name?: string;
485
+ }
486
+
480
487
  /** Append-only record of one committed transaction (a single mutation is a 1-entry transaction). */
481
488
  export declare interface MutationLogEntry {
482
489
  transactionId: string;
@@ -855,8 +862,9 @@ export declare interface TestClient {
855
862
  * Dispatch one or more release / version / document actions against the
856
863
  * store. Returns a synthetic `transactionId`. Multi-action arrays are
857
864
  * 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.
865
+ * publish/unpublish actions run through the access gate and expose their
866
+ * net draft/published mutations to `mutationGuard`; release/version actions
867
+ * apply unhooked.
860
868
  */
861
869
  action: (
862
870
  action: Action | Action[],
package/dist/index.d.ts CHANGED
@@ -152,12 +152,12 @@ export declare interface CreateTestClientOptions {
152
152
  */
153
153
  token?: string;
154
154
  /**
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}.
155
+ * Pluggable write interceptor — runs once for each document's net change in
156
+ * a commit. Document publish/unpublish actions expose their underlying
157
+ * draft/published mutations; release/version actions remain unhooked. Return
158
+ * denial descriptors to reject atomically with `GuardDeniedError`; a
159
+ * thrown callback error propagates unchanged. The client owns no policy.
160
+ * Inherited by `withConfig` siblings. See {@link MutationGuard}.
161
161
  */
162
162
  mutationGuard?: MutationGuard;
163
163
  /**
@@ -246,7 +246,11 @@ export declare interface DocumentBody {
246
246
  [field: string]: unknown;
247
247
  }
248
248
 
249
- export declare type DocumentValuePermission = "create" | "read" | "update";
249
+ export declare type DocumentValuePermission =
250
+ | "create"
251
+ | "manage"
252
+ | "read"
253
+ | "update";
250
254
 
251
255
  /**
252
256
  * Error helpers shared across the fake.
@@ -397,6 +401,30 @@ export declare interface GuardConfig {
397
401
  recorded?: Set<string>;
398
402
  }
399
403
 
404
+ /** HTTP-shaped guard rejection matching the error exposed by `@sanity/client`. */
405
+ export declare class GuardDeniedError extends Error {
406
+ readonly statusCode = 403;
407
+ readonly type = "guardDeniedError";
408
+ readonly denials: MutationGuardDenial[];
409
+ readonly details: GuardDeniedResponse["body"]["error"];
410
+ readonly response: GuardDeniedResponse;
411
+ constructor(denials: readonly MutationGuardDenial[]);
412
+ }
413
+
414
+ export declare interface GuardDeniedResponse {
415
+ body: {
416
+ error: {
417
+ type: "guardDeniedError";
418
+ denials: MutationGuardDenial[];
419
+ };
420
+ };
421
+ url: string;
422
+ method: "POST";
423
+ headers: Record<string, string>;
424
+ statusCode: 403;
425
+ statusMessage: "Forbidden";
426
+ }
427
+
400
428
  export declare const isDraftId: (id: string) => boolean;
401
429
 
402
430
  export declare const isReleaseDocId: (id: string) => boolean;
@@ -416,67 +444,46 @@ export declare interface MultipleMutationResult {
416
444
  }
417
445
 
418
446
  /**
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.
447
+ * Evaluates one net document change and returns every denying guard. Returning
448
+ * `void` allows observe-only hooks. A thrown value is a callback failure and
449
+ * propagates unchanged; only returned entries become {@link GuardDeniedError}.
435
450
  */
436
451
  export declare type MutationGuard = (
437
452
  context: MutationGuardContext,
438
- ) => void | Promise<void>;
453
+ ) =>
454
+ | readonly MutationGuardDenial[]
455
+ | void
456
+ | Promise<readonly MutationGuardDenial[] | void>;
439
457
 
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";
458
+ /** The Content Lake mutation actions that a {@link MutationGuard} can match. */
459
+ export declare type MutationGuardAction = "create" | "update" | "delete";
447
460
 
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
- */
461
+ /** The before/final-after image of one net document change in a commit. */
463
462
  export declare interface MutationGuardContext {
464
463
  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;
464
+ /** Original live store image. Treat it as read-only; it is not a copy. */
465
+ before: Readonly<
466
+ SanityDocumentLike & {
467
+ _id: string;
468
+ _type: string;
469
+ }
470
+ > | null;
471
+ /** Final live store image. Treat it as read-only; it is not a copy. */
472
+ after: Readonly<
473
+ SanityDocumentLike & {
474
+ _id: string;
475
+ _type: string;
476
+ }
477
+ > | null;
478
+ /** Acting project identity, or `undefined` on an identity-free client. */
477
479
  identity: string | undefined;
478
480
  }
479
481
 
482
+ export declare interface MutationGuardDenial {
483
+ guardID: string;
484
+ name?: string;
485
+ }
486
+
480
487
  /** Append-only record of one committed transaction (a single mutation is a 1-entry transaction). */
481
488
  export declare interface MutationLogEntry {
482
489
  transactionId: string;
@@ -855,8 +862,9 @@ export declare interface TestClient {
855
862
  * Dispatch one or more release / version / document actions against the
856
863
  * store. Returns a synthetic `transactionId`. Multi-action arrays are
857
864
  * 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.
865
+ * publish/unpublish actions run through the access gate and expose their
866
+ * net draft/published mutations to `mutationGuard`; release/version actions
867
+ * apply unhooked.
860
868
  */
861
869
  action: (
862
870
  action: Action | Action[],
package/dist/index.js CHANGED
@@ -151,6 +151,48 @@ function makeRequestImpl(ctx) {
151
151
  );
152
152
  };
153
153
  }
154
+ class GuardDeniedError extends Error {
155
+ statusCode = 403;
156
+ type = "guardDeniedError";
157
+ denials;
158
+ details;
159
+ response;
160
+ constructor(denials) {
161
+ const unique = uniqueDenials(denials);
162
+ super(`Mutation denied by guard(s) [${unique.map((denial) => denial.guardID).join(", ")}]`), this.name = "GuardDeniedError", this.denials = unique, this.details = { type: this.type, denials: unique }, this.response = {
163
+ body: { error: this.details },
164
+ url: "memory://client-fake-for-test/data/mutate",
165
+ method: "POST",
166
+ headers: {},
167
+ statusCode: this.statusCode,
168
+ statusMessage: "Forbidden"
169
+ };
170
+ }
171
+ }
172
+ function uniqueDenials(denials) {
173
+ const seen = /* @__PURE__ */ new Set();
174
+ return denials.filter((denial) => seen.has(denial.guardID) ? !1 : (seen.add(denial.guardID), !0));
175
+ }
176
+ function mutationContext(images) {
177
+ const { before, after } = images;
178
+ if (before !== after)
179
+ return before === void 0 ? { action: "create", before: null, after: after ?? null } : after === void 0 ? { action: "delete", before, after: null } : { action: "update", before, after };
180
+ }
181
+ function mutationContexts(images) {
182
+ return [...images].flatMap((image) => {
183
+ const context = mutationContext(image);
184
+ return context === void 0 ? [] : [context];
185
+ });
186
+ }
187
+ async function enforceMutationGuards(guard, contexts) {
188
+ if (guard === void 0) return;
189
+ const denials = [];
190
+ for (const context of contexts) {
191
+ const denied = await guard(context);
192
+ denied !== void 0 && denials.push(...denied);
193
+ }
194
+ if (denials.length > 0) throw new GuardDeniedError(denials);
195
+ }
154
196
  function describeType(value) {
155
197
  return value === null ? "null" : Array.isArray(value) ? "an array" : `a ${typeof value}`;
156
198
  }
@@ -684,7 +726,7 @@ async function applyAction(store, action, hooks) {
684
726
  const published = store.get(action.publishedId);
685
727
  requireRevisionMatch(action.draftId, draft._rev, action.ifDraftRevisionId), requireRevisionMatch(action.publishedId, published?._rev, action.ifPublishedRevisionId);
686
728
  const promoted = promote({ ...draft, _id: action.publishedId });
687
- return published !== void 0 && (promoted._createdAt = published._createdAt), await hooks?.gate?.(promoted, published === void 0 ? "create" : "update"), await hooks?.guard?.("publish", published ?? null, promoted), store.put(promoted), store.remove(action.draftId), { transactionId: cryptoRandomKey() };
729
+ return published !== void 0 && (promoted._createdAt = published._createdAt), await hooks?.gate?.(promoted, published === void 0 ? "create" : "update"), store.put(promoted), store.remove(action.draftId), { transactionId: cryptoRandomKey() };
688
730
  }
689
731
  case "sanity.action.document.unpublish": {
690
732
  requireDraftOf(action);
@@ -693,7 +735,7 @@ async function applyAction(store, action, hooks) {
693
735
  throw fakeError(
694
736
  `Cannot unpublish "${action.publishedId}": no published document with that _id exists. document.unpublish retracts a live published document into "${action.draftId}".`
695
737
  );
696
- if (await hooks?.gate?.(published, "update"), await hooks?.guard?.("unpublish", published, null), store.get(action.draftId) === void 0) {
738
+ if (await hooks?.gate?.(published, "update"), store.get(action.draftId) === void 0) {
697
739
  const draft = promote({ ...published, _id: action.draftId });
698
740
  draft._createdAt = published._createdAt, store.put(draft);
699
741
  }
@@ -714,7 +756,7 @@ function requireRevisionMatch(documentId, actualRev, expectedRev) {
714
756
  );
715
757
  }
716
758
  async function applyActions(store, actions, hooks) {
717
- const list = Array.isArray(actions) ? actions : [actions], snapshot = /* @__PURE__ */ new Map(), remember = (id) => {
759
+ const list = Array.isArray(actions) ? actions : [actions], snapshot = /* @__PURE__ */ new Map(), guardedIds = /* @__PURE__ */ new Set(), remember = (id) => {
718
760
  snapshot.has(id) || snapshot.set(id, store.get(id));
719
761
  };
720
762
  try {
@@ -750,13 +792,18 @@ async function applyActions(store, actions, hooks) {
750
792
  remember(a.versionId);
751
793
  break;
752
794
  case "sanity.action.document.publish":
795
+ remember(a.draftId), remember(a.publishedId), guardedIds.add(a.publishedId), guardedIds.add(a.draftId);
796
+ break;
753
797
  case "sanity.action.document.unpublish":
754
- remember(a.draftId), remember(a.publishedId);
798
+ remember(a.draftId), remember(a.publishedId), guardedIds.add(a.draftId), guardedIds.add(a.publishedId);
755
799
  break;
756
800
  }
757
801
  last = await applyAction(store, a, hooks);
758
802
  }
759
- return last;
803
+ const contexts = mutationContexts(
804
+ [...guardedIds].map((id) => ({ before: snapshot.get(id), after: store.get(id) }))
805
+ );
806
+ return await hooks?.guard?.(contexts), last;
760
807
  } catch (err) {
761
808
  for (const [id, doc] of snapshot)
762
809
  doc === void 0 ? store.remove(id) : store.put(doc);
@@ -1187,11 +1234,17 @@ function makeHandle(handleArgs) {
1187
1234
  access !== void 0 && await enforceWriteAccess({
1188
1235
  document,
1189
1236
  grants: access.grants,
1190
- permission,
1237
+ permission: document._id !== void 0 && isSystemId(document._id) ? "manage" : permission,
1191
1238
  userId: access.currentUser.id
1192
1239
  });
1193
- }, guardGate = async (action, before, after) => {
1194
- mutationGuard !== void 0 && await mutationGuard({ action, before, after, identity: activeAccess()?.currentUser.id });
1240
+ }, guardGate = async (contexts) => {
1241
+ const identity = activeAccess()?.currentUser.id;
1242
+ await enforceMutationGuards(
1243
+ mutationGuard,
1244
+ contexts.map((context) => ({ ...context, identity }))
1245
+ );
1246
+ }, guardOne = async (action, before, after) => {
1247
+ await guardGate([{ action, before, after }]);
1195
1248
  }, canRead = async (document) => {
1196
1249
  const access = activeAccess();
1197
1250
  return access === void 0 ? !0 : canAccess({
@@ -1200,7 +1253,7 @@ function makeHandle(handleArgs) {
1200
1253
  permission: "read",
1201
1254
  userId: access.currentUser.id
1202
1255
  });
1203
- }, applyPatch = async (documentId, ops, ifRev) => {
1256
+ }, applyPatch = async (documentId, ops, ifRev, options = {}) => {
1204
1257
  const existing = store.get(documentId);
1205
1258
  if (existing === void 0)
1206
1259
  throw fakeError(
@@ -1212,7 +1265,7 @@ function makeHandle(handleArgs) {
1212
1265
  _rev: cryptoRandomKey(),
1213
1266
  _updatedAt: now
1214
1267
  };
1215
- return await guardGate("update", existing, next), store.put(next), next;
1268
+ return options.evaluateGuard !== !1 && await guardOne("update", existing, next), store.put(next), next;
1216
1269
  }, applyTransaction = async (mutations, txnOptions) => {
1217
1270
  const transactionId = txnOptions?.transactionId ?? cryptoRandomKey(), snapshot = /* @__PURE__ */ new Map(), touchedIds = [], operations = [], remember = (id) => {
1218
1271
  snapshot.has(id) || snapshot.set(id, store.get(id));
@@ -1227,17 +1280,13 @@ function makeHandle(handleArgs) {
1227
1280
  throw fakeError(
1228
1281
  `create() failed: a document with _id "${promoted._id}" already exists in dataset "${projectId}:${dataset}". Use createOrReplace() to overwrite it, or createIfNotExists() to leave the existing one untouched.`
1229
1282
  );
1230
- await gate(promoted, "create"), await guardGate("create", null, promoted), store.put(promoted), touchedIds.push(promoted._id), operations.push({ id: promoted._id, operation: "create" });
1283
+ await gate(promoted, "create"), store.put(promoted), touchedIds.push(promoted._id), operations.push({ id: promoted._id, operation: "create" });
1231
1284
  break;
1232
1285
  }
1233
1286
  case "createOrReplace": {
1234
1287
  remember(mut.doc._id);
1235
1288
  const promoted = promote2(mut.doc), existing = store.get(promoted._id);
1236
- await gate(existing ?? promoted, existing !== void 0 ? "update" : "create"), await guardGate(
1237
- existing !== void 0 ? "update" : "create",
1238
- existing ?? null,
1239
- promoted
1240
- );
1289
+ await gate(existing ?? promoted, existing !== void 0 ? "update" : "create");
1241
1290
  const wasPresent = existing !== void 0;
1242
1291
  store.put(promoted), touchedIds.push(promoted._id), operations.push({ id: promoted._id, operation: wasPresent ? "update" : "create" });
1243
1292
  break;
@@ -1245,21 +1294,25 @@ function makeHandle(handleArgs) {
1245
1294
  case "createIfNotExists": {
1246
1295
  if (remember(mut.doc._id), store.get(mut.doc._id) === void 0) {
1247
1296
  const promoted = promote2(mut.doc);
1248
- await gate(promoted, "create"), await guardGate("create", null, promoted), store.put(promoted), touchedIds.push(promoted._id), operations.push({ id: promoted._id, operation: "create" });
1297
+ await gate(promoted, "create"), store.put(promoted), touchedIds.push(promoted._id), operations.push({ id: promoted._id, operation: "create" });
1249
1298
  }
1250
1299
  break;
1251
1300
  }
1252
1301
  case "delete": {
1253
1302
  remember(mut.id);
1254
1303
  const existing = store.get(mut.id);
1255
- existing !== void 0 && await gate(existing, "update"), existing !== void 0 && await guardGate("delete", existing, null), store.remove(mut.id) && (touchedIds.push(mut.id), operations.push({ id: mut.id, operation: "delete" }));
1304
+ existing !== void 0 && await gate(existing, "update"), store.remove(mut.id) && (touchedIds.push(mut.id), operations.push({ id: mut.id, operation: "delete" }));
1256
1305
  break;
1257
1306
  }
1258
1307
  case "patch": {
1259
- remember(mut.documentId), await applyPatch(mut.documentId, mut.ops, mut.ifRev), touchedIds.push(mut.documentId), operations.push({ id: mut.documentId, operation: "update" });
1308
+ remember(mut.documentId), await applyPatch(mut.documentId, mut.ops, mut.ifRev, { evaluateGuard: !1 }), touchedIds.push(mut.documentId), operations.push({ id: mut.documentId, operation: "update" });
1260
1309
  break;
1261
1310
  }
1262
1311
  }
1312
+ const contexts = mutationContexts(
1313
+ [...snapshot].map(([id, before]) => ({ before, after: store.get(id) }))
1314
+ );
1315
+ await guardGate(contexts);
1263
1316
  } catch (error) {
1264
1317
  for (const [id, doc] of snapshot)
1265
1318
  doc === void 0 ? store.remove(id) : store.put(doc);
@@ -1324,7 +1377,7 @@ function makeHandle(handleArgs) {
1324
1377
  `create() failed: a document with _id "${doc._id}" already exists in dataset "${projectId}:${dataset}". Use createOrReplace() to overwrite it, or createIfNotExists() to leave the existing one untouched.`
1325
1378
  );
1326
1379
  const promoted = promote2(doc);
1327
- await gate(promoted, "create"), await guardGate("create", null, promoted);
1380
+ await gate(promoted, "create"), await guardOne("create", null, promoted);
1328
1381
  const transactionId = opts?.transactionId ?? cryptoRandomKey();
1329
1382
  return opts?.dryRun !== !0 && (store.put(promoted), record(transactionId, [{ kind: "create", doc }], opts?.tag)), writeResult(
1330
1383
  [promoted],
@@ -1336,7 +1389,7 @@ function makeHandle(handleArgs) {
1336
1389
  createOrReplace: (doc, opts) => serializeWrite(backend, async () => {
1337
1390
  checkRevision(doc._id, opts?.ifRevisionId);
1338
1391
  const promoted = promote2(doc), existing = store.get(doc._id);
1339
- await gate(existing ?? promoted, existing !== void 0 ? "update" : "create"), await guardGate(existing !== void 0 ? "update" : "create", existing ?? null, promoted);
1392
+ await gate(existing ?? promoted, existing !== void 0 ? "update" : "create"), await guardOne(existing !== void 0 ? "update" : "create", existing ?? null, promoted);
1340
1393
  const transactionId = opts?.transactionId ?? cryptoRandomKey();
1341
1394
  opts?.dryRun !== !0 && (store.put(promoted), record(transactionId, [{ kind: "createOrReplace", doc }], opts?.tag));
1342
1395
  const operation = existing !== void 0 ? "update" : "create";
@@ -1358,7 +1411,7 @@ function makeHandle(handleArgs) {
1358
1411
  opts
1359
1412
  );
1360
1413
  const promoted = promote2(doc);
1361
- return await gate(promoted, "create"), await guardGate("create", null, promoted), opts?.dryRun !== !0 && (store.put(promoted), record(transactionId, [{ kind: "createIfNotExists", doc }], opts?.tag)), writeResult(
1414
+ return await gate(promoted, "create"), await guardOne("create", null, promoted), opts?.dryRun !== !0 && (store.put(promoted), record(transactionId, [{ kind: "createIfNotExists", doc }], opts?.tag)), writeResult(
1362
1415
  [promoted],
1363
1416
  [{ id: promoted._id, operation: "create" }],
1364
1417
  transactionId,
@@ -1368,7 +1421,7 @@ function makeHandle(handleArgs) {
1368
1421
  delete: (id, opts) => serializeWrite(backend, async () => {
1369
1422
  checkRevision(id, opts?.ifRevisionId);
1370
1423
  const existing = store.get(id);
1371
- existing !== void 0 && await gate(existing, "update"), existing !== void 0 && await guardGate("delete", existing, null);
1424
+ existing !== void 0 && await gate(existing, "update"), existing !== void 0 && await guardOne("delete", existing, null);
1372
1425
  const transactionId = opts?.transactionId ?? cryptoRandomKey();
1373
1426
  let removed = existing;
1374
1427
  return opts?.dryRun !== !0 && (removed = store.remove(id), removed !== void 0 && record(transactionId, [{ kind: "delete", id }], opts?.tag)), opts?.returnDocuments === void 0 && opts?.returnFirst === void 0 ? removed !== void 0 ? { _id: id } : null : formatMutationResult(
@@ -1519,6 +1572,7 @@ function makeHandle(handleArgs) {
1519
1572
  }
1520
1573
  export {
1521
1574
  ERR_PREFIX,
1575
+ GuardDeniedError,
1522
1576
  SUPPORTED_SURFACE,
1523
1577
  UNSUPPORTED_MEMBERS,
1524
1578
  WriteAccessDeniedError,