@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/README.md +11 -4
- package/dist/index.cjs +77 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +68 -60
- package/dist/index.d.ts +68 -60
- package/dist/index.js +77 -23
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
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 =
|
|
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
|
-
*
|
|
420
|
-
* `
|
|
421
|
-
*
|
|
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
|
-
) =>
|
|
453
|
+
) =>
|
|
454
|
+
| readonly MutationGuardDenial[]
|
|
455
|
+
| void
|
|
456
|
+
| Promise<readonly MutationGuardDenial[] | void>;
|
|
439
457
|
|
|
440
|
-
/** The
|
|
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
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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
|
|
859
|
-
* `mutationGuard
|
|
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
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
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 =
|
|
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
|
-
*
|
|
420
|
-
* `
|
|
421
|
-
*
|
|
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
|
-
) =>
|
|
453
|
+
) =>
|
|
454
|
+
| readonly MutationGuardDenial[]
|
|
455
|
+
| void
|
|
456
|
+
| Promise<readonly MutationGuardDenial[] | void>;
|
|
439
457
|
|
|
440
|
-
/** The
|
|
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
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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
|
|
859
|
-
* `mutationGuard
|
|
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"),
|
|
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"),
|
|
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
|
-
|
|
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 (
|
|
1194
|
-
|
|
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
|
|
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"),
|
|
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")
|
|
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"),
|
|
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"),
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|