@sanity-labs/client-fake-for-test 0.3.0 → 0.5.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 +39 -1
- package/dist/index.cjs +38 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +78 -0
- package/dist/index.d.ts +78 -0
- package/dist/index.js +38 -15
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -110,6 +110,15 @@ export declare interface CreateTestClientOptions {
|
|
|
110
110
|
* unconditionally — the legacy behavior.
|
|
111
111
|
*/
|
|
112
112
|
accessControl?: TestClientAccessControl;
|
|
113
|
+
/**
|
|
114
|
+
* Pluggable write interceptor — runs on each write through the document
|
|
115
|
+
* mutation API (create/patch/delete/transaction; not the release/version
|
|
116
|
+
* action API) before the store changes, and may throw to reject it. The
|
|
117
|
+
* client has no opinion about the policy; callers (e.g. a workflow test bench
|
|
118
|
+
* enforcing lake mutation guards) supply it. Inherited by `withConfig`
|
|
119
|
+
* siblings. See {@link MutationGuard}.
|
|
120
|
+
*/
|
|
121
|
+
mutationGuard?: MutationGuard;
|
|
113
122
|
/**
|
|
114
123
|
* Default perspective applied to reads. Per-fetch `options.perspective`
|
|
115
124
|
* overrides this. `withConfig({ perspective })` returns a sibling
|
|
@@ -259,6 +268,8 @@ export declare type FakeMutation =
|
|
|
259
268
|
*
|
|
260
269
|
* - `seed`, `add`, `reset`, `snapshot`, `store` — bulk-load helpers,
|
|
261
270
|
* scoped to the current dataset.
|
|
271
|
+
* - `simulateConcurrentEdit()` — force the next N revision-guarded writes
|
|
272
|
+
* to a document to 409, for optimistic-locking retry tests.
|
|
262
273
|
* - `datasets()` — lists known dataset keys in this registry.
|
|
263
274
|
*
|
|
264
275
|
* Methods deliberately omitted:
|
|
@@ -329,6 +340,58 @@ export declare interface MultipleMutationResult {
|
|
|
329
340
|
}[];
|
|
330
341
|
}
|
|
331
342
|
|
|
343
|
+
/**
|
|
344
|
+
* A pluggable write interceptor, configured via
|
|
345
|
+
* `createTestClient({ mutationGuard })`. It runs on each write through the
|
|
346
|
+
* document mutation API — `create` / `createOrReplace` / `createIfNotExists` /
|
|
347
|
+
* `patch` / `delete` and the equivalent `transaction()` operations — before the
|
|
348
|
+
* store changes; **throw to reject the write** (the throw propagates out of the
|
|
349
|
+
* client call, exactly as a real lake rejection would). The release/version
|
|
350
|
+
* action API (`client.action`, `client.releases.*`, `createVersion`) is **not**
|
|
351
|
+
* intercepted.
|
|
352
|
+
*
|
|
353
|
+
* The client itself has no opinion about *what* a guard is — it only provides
|
|
354
|
+
* the seam and the before/after/identity context. Callers (e.g. a workflow test
|
|
355
|
+
* bench) supply the policy: evaluate lake mutation guards, enforce field locks,
|
|
356
|
+
* etc. Keeping the policy in the caller avoids duplicating guard-evaluation
|
|
357
|
+
* logic the client has no business owning.
|
|
358
|
+
*/
|
|
359
|
+
export declare type MutationGuard = (
|
|
360
|
+
context: MutationGuardContext,
|
|
361
|
+
) => void | Promise<void>;
|
|
362
|
+
|
|
363
|
+
/** The kind of write a {@link MutationGuard} is asked to vet. */
|
|
364
|
+
export declare type MutationGuardAction = "create" | "update" | "delete";
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* The before/after image of a single document write, handed to a
|
|
368
|
+
* {@link MutationGuard} before it is applied.
|
|
369
|
+
*
|
|
370
|
+
* - `before` is null on create; `after` is null on delete.
|
|
371
|
+
* - `identity` is the acting user — the id bound to the client's token
|
|
372
|
+
* (its `accessControl.currentUser`), or undefined when the client has no
|
|
373
|
+
* identity configured.
|
|
374
|
+
*
|
|
375
|
+
* Treat `before`/`after` as read-only — they are the live store images, not
|
|
376
|
+
* copies; a guard that mutates them corrupts the store.
|
|
377
|
+
*/
|
|
378
|
+
export declare interface MutationGuardContext {
|
|
379
|
+
action: MutationGuardAction;
|
|
380
|
+
before:
|
|
381
|
+
| (SanityDocumentLike & {
|
|
382
|
+
_id: string;
|
|
383
|
+
_type: string;
|
|
384
|
+
})
|
|
385
|
+
| null;
|
|
386
|
+
after:
|
|
387
|
+
| (SanityDocumentLike & {
|
|
388
|
+
_id: string;
|
|
389
|
+
_type: string;
|
|
390
|
+
})
|
|
391
|
+
| null;
|
|
392
|
+
identity: string | undefined;
|
|
393
|
+
}
|
|
394
|
+
|
|
332
395
|
/** Append-only record of one committed transaction (a single mutation is a 1-entry transaction). */
|
|
333
396
|
export declare interface MutationLogEntry {
|
|
334
397
|
transactionId: string;
|
|
@@ -706,6 +769,12 @@ export declare interface TestClient {
|
|
|
706
769
|
projectId: string;
|
|
707
770
|
dataset: string;
|
|
708
771
|
perspective: Perspective;
|
|
772
|
+
/**
|
|
773
|
+
* Rebind the acting identity (and its grants) on a sibling that shares
|
|
774
|
+
* the same store — the test analogue of a token-scoped client. Lets two
|
|
775
|
+
* actors write the same dataset, e.g. for `identity()`-based guards.
|
|
776
|
+
*/
|
|
777
|
+
accessControl: TestClientAccessControl;
|
|
709
778
|
}>,
|
|
710
779
|
) => TestClient;
|
|
711
780
|
/** Replace the current dataset with the given documents. */
|
|
@@ -718,6 +787,15 @@ export declare interface TestClient {
|
|
|
718
787
|
snapshot: () => readonly SanityDocument[];
|
|
719
788
|
/** Direct store access for the current dataset. */
|
|
720
789
|
store: () => TestStore;
|
|
790
|
+
/**
|
|
791
|
+
* Simulate `count` concurrent edits to `id` in the current dataset: the
|
|
792
|
+
* next `count` revision-guarded mutations (those passing `ifRevisionId`)
|
|
793
|
+
* to that document throw a 409-shaped conflict, as if another client
|
|
794
|
+
* committed between the caller's read and write. Lets a test exercise
|
|
795
|
+
* optimistic-locking retry paths deterministically, without timing or
|
|
796
|
+
* interleaving. Mutations that don't pass `ifRevisionId` are unaffected.
|
|
797
|
+
*/
|
|
798
|
+
simulateConcurrentEdit: (id: string, count?: number) => void;
|
|
721
799
|
/** List `(projectId, dataset)` pairs known in this registry. */
|
|
722
800
|
datasets: () => {
|
|
723
801
|
projectId: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -110,6 +110,15 @@ export declare interface CreateTestClientOptions {
|
|
|
110
110
|
* unconditionally — the legacy behavior.
|
|
111
111
|
*/
|
|
112
112
|
accessControl?: TestClientAccessControl;
|
|
113
|
+
/**
|
|
114
|
+
* Pluggable write interceptor — runs on each write through the document
|
|
115
|
+
* mutation API (create/patch/delete/transaction; not the release/version
|
|
116
|
+
* action API) before the store changes, and may throw to reject it. The
|
|
117
|
+
* client has no opinion about the policy; callers (e.g. a workflow test bench
|
|
118
|
+
* enforcing lake mutation guards) supply it. Inherited by `withConfig`
|
|
119
|
+
* siblings. See {@link MutationGuard}.
|
|
120
|
+
*/
|
|
121
|
+
mutationGuard?: MutationGuard;
|
|
113
122
|
/**
|
|
114
123
|
* Default perspective applied to reads. Per-fetch `options.perspective`
|
|
115
124
|
* overrides this. `withConfig({ perspective })` returns a sibling
|
|
@@ -259,6 +268,8 @@ export declare type FakeMutation =
|
|
|
259
268
|
*
|
|
260
269
|
* - `seed`, `add`, `reset`, `snapshot`, `store` — bulk-load helpers,
|
|
261
270
|
* scoped to the current dataset.
|
|
271
|
+
* - `simulateConcurrentEdit()` — force the next N revision-guarded writes
|
|
272
|
+
* to a document to 409, for optimistic-locking retry tests.
|
|
262
273
|
* - `datasets()` — lists known dataset keys in this registry.
|
|
263
274
|
*
|
|
264
275
|
* Methods deliberately omitted:
|
|
@@ -329,6 +340,58 @@ export declare interface MultipleMutationResult {
|
|
|
329
340
|
}[];
|
|
330
341
|
}
|
|
331
342
|
|
|
343
|
+
/**
|
|
344
|
+
* A pluggable write interceptor, configured via
|
|
345
|
+
* `createTestClient({ mutationGuard })`. It runs on each write through the
|
|
346
|
+
* document mutation API — `create` / `createOrReplace` / `createIfNotExists` /
|
|
347
|
+
* `patch` / `delete` and the equivalent `transaction()` operations — before the
|
|
348
|
+
* store changes; **throw to reject the write** (the throw propagates out of the
|
|
349
|
+
* client call, exactly as a real lake rejection would). The release/version
|
|
350
|
+
* action API (`client.action`, `client.releases.*`, `createVersion`) is **not**
|
|
351
|
+
* intercepted.
|
|
352
|
+
*
|
|
353
|
+
* The client itself has no opinion about *what* a guard is — it only provides
|
|
354
|
+
* the seam and the before/after/identity context. Callers (e.g. a workflow test
|
|
355
|
+
* bench) supply the policy: evaluate lake mutation guards, enforce field locks,
|
|
356
|
+
* etc. Keeping the policy in the caller avoids duplicating guard-evaluation
|
|
357
|
+
* logic the client has no business owning.
|
|
358
|
+
*/
|
|
359
|
+
export declare type MutationGuard = (
|
|
360
|
+
context: MutationGuardContext,
|
|
361
|
+
) => void | Promise<void>;
|
|
362
|
+
|
|
363
|
+
/** The kind of write a {@link MutationGuard} is asked to vet. */
|
|
364
|
+
export declare type MutationGuardAction = "create" | "update" | "delete";
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* The before/after image of a single document write, handed to a
|
|
368
|
+
* {@link MutationGuard} before it is applied.
|
|
369
|
+
*
|
|
370
|
+
* - `before` is null on create; `after` is null on delete.
|
|
371
|
+
* - `identity` is the acting user — the id bound to the client's token
|
|
372
|
+
* (its `accessControl.currentUser`), or undefined when the client has no
|
|
373
|
+
* identity configured.
|
|
374
|
+
*
|
|
375
|
+
* Treat `before`/`after` as read-only — they are the live store images, not
|
|
376
|
+
* copies; a guard that mutates them corrupts the store.
|
|
377
|
+
*/
|
|
378
|
+
export declare interface MutationGuardContext {
|
|
379
|
+
action: MutationGuardAction;
|
|
380
|
+
before:
|
|
381
|
+
| (SanityDocumentLike & {
|
|
382
|
+
_id: string;
|
|
383
|
+
_type: string;
|
|
384
|
+
})
|
|
385
|
+
| null;
|
|
386
|
+
after:
|
|
387
|
+
| (SanityDocumentLike & {
|
|
388
|
+
_id: string;
|
|
389
|
+
_type: string;
|
|
390
|
+
})
|
|
391
|
+
| null;
|
|
392
|
+
identity: string | undefined;
|
|
393
|
+
}
|
|
394
|
+
|
|
332
395
|
/** Append-only record of one committed transaction (a single mutation is a 1-entry transaction). */
|
|
333
396
|
export declare interface MutationLogEntry {
|
|
334
397
|
transactionId: string;
|
|
@@ -706,6 +769,12 @@ export declare interface TestClient {
|
|
|
706
769
|
projectId: string;
|
|
707
770
|
dataset: string;
|
|
708
771
|
perspective: Perspective;
|
|
772
|
+
/**
|
|
773
|
+
* Rebind the acting identity (and its grants) on a sibling that shares
|
|
774
|
+
* the same store — the test analogue of a token-scoped client. Lets two
|
|
775
|
+
* actors write the same dataset, e.g. for `identity()`-based guards.
|
|
776
|
+
*/
|
|
777
|
+
accessControl: TestClientAccessControl;
|
|
709
778
|
}>,
|
|
710
779
|
) => TestClient;
|
|
711
780
|
/** Replace the current dataset with the given documents. */
|
|
@@ -718,6 +787,15 @@ export declare interface TestClient {
|
|
|
718
787
|
snapshot: () => readonly SanityDocument[];
|
|
719
788
|
/** Direct store access for the current dataset. */
|
|
720
789
|
store: () => TestStore;
|
|
790
|
+
/**
|
|
791
|
+
* Simulate `count` concurrent edits to `id` in the current dataset: the
|
|
792
|
+
* next `count` revision-guarded mutations (those passing `ifRevisionId`)
|
|
793
|
+
* to that document throw a 409-shaped conflict, as if another client
|
|
794
|
+
* committed between the caller's read and write. Lets a test exercise
|
|
795
|
+
* optimistic-locking retry paths deterministically, without timing or
|
|
796
|
+
* interleaving. Mutations that don't pass `ifRevisionId` are unaffected.
|
|
797
|
+
*/
|
|
798
|
+
simulateConcurrentEdit: (id: string, count?: number) => void;
|
|
721
799
|
/** List `(projectId, dataset)` pairs known in this registry. */
|
|
722
800
|
datasets: () => {
|
|
723
801
|
projectId: string;
|
package/dist/index.js
CHANGED
|
@@ -859,7 +859,13 @@ const RECORDED_METHODS = /* @__PURE__ */ new Set([
|
|
|
859
859
|
return { projectId, dataset };
|
|
860
860
|
};
|
|
861
861
|
function createTestClient(options = {}) {
|
|
862
|
-
const defaultProjectId = options.projectId ?? DEFAULT_PROJECT_ID, defaultDataset = options.dataset ?? DEFAULT_DATASET, backend = {
|
|
862
|
+
const defaultProjectId = options.projectId ?? DEFAULT_PROJECT_ID, defaultDataset = options.dataset ?? DEFAULT_DATASET, backend = {
|
|
863
|
+
registry: /* @__PURE__ */ new Map(),
|
|
864
|
+
log: [],
|
|
865
|
+
stubs: /* @__PURE__ */ new Map(),
|
|
866
|
+
calls: [],
|
|
867
|
+
concurrentEdits: /* @__PURE__ */ new Map()
|
|
868
|
+
}, registry = backend.registry, ensureStore = (projectId, dataset) => {
|
|
863
869
|
const key = registryKey(projectId, dataset);
|
|
864
870
|
let store = registry.get(key);
|
|
865
871
|
return store === void 0 && (store = createTestStore(), registry.set(key, store)), store;
|
|
@@ -874,7 +880,8 @@ function createTestClient(options = {}) {
|
|
|
874
880
|
defaultProjectId,
|
|
875
881
|
defaultDataset,
|
|
876
882
|
options.accessControl,
|
|
877
|
-
options.perspective ?? "raw"
|
|
883
|
+
options.perspective ?? "raw",
|
|
884
|
+
options.mutationGuard
|
|
878
885
|
);
|
|
879
886
|
}
|
|
880
887
|
function dedupeIds(results) {
|
|
@@ -927,12 +934,12 @@ function toTxnMutations(operations) {
|
|
|
927
934
|
}
|
|
928
935
|
];
|
|
929
936
|
}
|
|
930
|
-
function makeHandle(backend, projectId, dataset, accessControl, perspective) {
|
|
937
|
+
function makeHandle(backend, projectId, dataset, accessControl, perspective, mutationGuard) {
|
|
931
938
|
const { registry } = backend, store = (() => {
|
|
932
939
|
const key = registryKey(projectId, dataset);
|
|
933
940
|
let s = registry.get(key);
|
|
934
941
|
return s === void 0 && (s = createTestStore(), registry.set(key, s)), s;
|
|
935
|
-
})(), record = (transactionId, mutations) => {
|
|
942
|
+
})(), concurrentEditKey = (id) => registryKey(projectId, dataset) + " " + id, record = (transactionId, mutations) => {
|
|
936
943
|
backend.log.push({ transactionId, projectId, dataset, mutations });
|
|
937
944
|
}, writeResult = (documents, results, transactionId, opts) => formatMutationResult(
|
|
938
945
|
documents,
|
|
@@ -960,6 +967,11 @@ function makeHandle(backend, projectId, dataset, accessControl, perspective) {
|
|
|
960
967
|
};
|
|
961
968
|
}, checkRevision = (id, ifRevisionId) => {
|
|
962
969
|
if (ifRevisionId === void 0) return;
|
|
970
|
+
const editKey = concurrentEditKey(id), pendingEdits = backend.concurrentEdits.get(editKey) ?? 0;
|
|
971
|
+
if (pendingEdits > 0)
|
|
972
|
+
throw backend.concurrentEdits.set(editKey, pendingEdits - 1), fakeError(
|
|
973
|
+
`ifRevisionId check failed: document "${id}" was concurrently edited (simulated via simulateConcurrentEdit) \u2014 the revision-guarded mutation lost the race; re-read and retry. (This mirrors Sanity's optimistic-locking 409.)`
|
|
974
|
+
);
|
|
963
975
|
const existing = store.get(id);
|
|
964
976
|
if (existing === void 0)
|
|
965
977
|
throw fakeError(
|
|
@@ -976,6 +988,8 @@ function makeHandle(backend, projectId, dataset, accessControl, perspective) {
|
|
|
976
988
|
permission,
|
|
977
989
|
userId: accessControl.currentUser.id
|
|
978
990
|
});
|
|
991
|
+
}, guardGate = async (action, before, after) => {
|
|
992
|
+
mutationGuard !== void 0 && await mutationGuard({ action, before, after, identity: accessControl?.currentUser.id });
|
|
979
993
|
}, canRead = async (document) => accessControl === void 0 ? !0 : canAccess({
|
|
980
994
|
document,
|
|
981
995
|
grants: accessControl.grants,
|
|
@@ -993,7 +1007,7 @@ function makeHandle(backend, projectId, dataset, accessControl, perspective) {
|
|
|
993
1007
|
_rev: cryptoRandomKey(),
|
|
994
1008
|
_updatedAt: now
|
|
995
1009
|
};
|
|
996
|
-
return store.put(next), next;
|
|
1010
|
+
return await guardGate("update", existing, next), store.put(next), next;
|
|
997
1011
|
}, applyTransaction = async (mutations, txnOptions) => {
|
|
998
1012
|
const transactionId = txnOptions?.transactionId ?? cryptoRandomKey(), snapshot = /* @__PURE__ */ new Map(), touchedIds = [], operations = [], remember = (id) => {
|
|
999
1013
|
snapshot.has(id) || snapshot.set(id, store.get(id));
|
|
@@ -1008,13 +1022,17 @@ function makeHandle(backend, projectId, dataset, accessControl, perspective) {
|
|
|
1008
1022
|
throw fakeError(
|
|
1009
1023
|
`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.`
|
|
1010
1024
|
);
|
|
1011
|
-
await gate(promoted, "create"), store.put(promoted), touchedIds.push(promoted._id), operations.push({ id: promoted._id, operation: "create" });
|
|
1025
|
+
await gate(promoted, "create"), await guardGate("create", null, promoted), store.put(promoted), touchedIds.push(promoted._id), operations.push({ id: promoted._id, operation: "create" });
|
|
1012
1026
|
break;
|
|
1013
1027
|
}
|
|
1014
1028
|
case "createOrReplace": {
|
|
1015
1029
|
remember(mut.doc._id);
|
|
1016
1030
|
const promoted = promote2(mut.doc), existing = store.get(promoted._id);
|
|
1017
|
-
await gate(existing ?? promoted, existing !== void 0 ? "update" : "create")
|
|
1031
|
+
await gate(existing ?? promoted, existing !== void 0 ? "update" : "create"), await guardGate(
|
|
1032
|
+
existing !== void 0 ? "update" : "create",
|
|
1033
|
+
existing ?? null,
|
|
1034
|
+
promoted
|
|
1035
|
+
);
|
|
1018
1036
|
const wasPresent = existing !== void 0;
|
|
1019
1037
|
store.put(promoted), touchedIds.push(promoted._id), operations.push({ id: promoted._id, operation: wasPresent ? "update" : "create" });
|
|
1020
1038
|
break;
|
|
@@ -1022,14 +1040,14 @@ function makeHandle(backend, projectId, dataset, accessControl, perspective) {
|
|
|
1022
1040
|
case "createIfNotExists": {
|
|
1023
1041
|
if (remember(mut.doc._id), store.get(mut.doc._id) === void 0) {
|
|
1024
1042
|
const promoted = promote2(mut.doc);
|
|
1025
|
-
await gate(promoted, "create"), store.put(promoted), touchedIds.push(promoted._id), operations.push({ id: promoted._id, operation: "create" });
|
|
1043
|
+
await gate(promoted, "create"), await guardGate("create", null, promoted), store.put(promoted), touchedIds.push(promoted._id), operations.push({ id: promoted._id, operation: "create" });
|
|
1026
1044
|
}
|
|
1027
1045
|
break;
|
|
1028
1046
|
}
|
|
1029
1047
|
case "delete": {
|
|
1030
1048
|
remember(mut.id);
|
|
1031
1049
|
const existing = store.get(mut.id);
|
|
1032
|
-
existing !== void 0 && await gate(existing, "update"), store.remove(mut.id) && (touchedIds.push(mut.id), operations.push({ id: mut.id, operation: "delete" }));
|
|
1050
|
+
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" }));
|
|
1033
1051
|
break;
|
|
1034
1052
|
}
|
|
1035
1053
|
case "patch": {
|
|
@@ -1101,7 +1119,7 @@ function makeHandle(backend, projectId, dataset, accessControl, perspective) {
|
|
|
1101
1119
|
`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.`
|
|
1102
1120
|
);
|
|
1103
1121
|
const promoted = promote2(doc);
|
|
1104
|
-
await gate(promoted, "create");
|
|
1122
|
+
await gate(promoted, "create"), await guardGate("create", null, promoted);
|
|
1105
1123
|
const transactionId = opts?.transactionId ?? cryptoRandomKey();
|
|
1106
1124
|
return opts?.dryRun !== !0 && (store.put(promoted), record(transactionId, [{ kind: "create", doc }])), writeResult(
|
|
1107
1125
|
[promoted],
|
|
@@ -1113,7 +1131,7 @@ function makeHandle(backend, projectId, dataset, accessControl, perspective) {
|
|
|
1113
1131
|
createOrReplace: async (doc, opts) => {
|
|
1114
1132
|
checkRevision(doc._id, opts?.ifRevisionId);
|
|
1115
1133
|
const promoted = promote2(doc), existing = store.get(doc._id);
|
|
1116
|
-
await gate(existing ?? promoted, existing !== void 0 ? "update" : "create");
|
|
1134
|
+
await gate(existing ?? promoted, existing !== void 0 ? "update" : "create"), await guardGate(existing !== void 0 ? "update" : "create", existing ?? null, promoted);
|
|
1117
1135
|
const transactionId = opts?.transactionId ?? cryptoRandomKey();
|
|
1118
1136
|
opts?.dryRun !== !0 && (store.put(promoted), record(transactionId, [{ kind: "createOrReplace", doc }]));
|
|
1119
1137
|
const operation = existing !== void 0 ? "update" : "create";
|
|
@@ -1135,7 +1153,7 @@ function makeHandle(backend, projectId, dataset, accessControl, perspective) {
|
|
|
1135
1153
|
opts
|
|
1136
1154
|
);
|
|
1137
1155
|
const promoted = promote2(doc);
|
|
1138
|
-
return await gate(promoted, "create"), opts?.dryRun !== !0 && (store.put(promoted), record(transactionId, [{ kind: "createIfNotExists", doc }])), writeResult(
|
|
1156
|
+
return await gate(promoted, "create"), await guardGate("create", null, promoted), opts?.dryRun !== !0 && (store.put(promoted), record(transactionId, [{ kind: "createIfNotExists", doc }])), writeResult(
|
|
1139
1157
|
[promoted],
|
|
1140
1158
|
[{ id: promoted._id, operation: "create" }],
|
|
1141
1159
|
transactionId,
|
|
@@ -1145,7 +1163,7 @@ function makeHandle(backend, projectId, dataset, accessControl, perspective) {
|
|
|
1145
1163
|
delete: async (id, opts) => {
|
|
1146
1164
|
checkRevision(id, opts?.ifRevisionId);
|
|
1147
1165
|
const existing = store.get(id);
|
|
1148
|
-
existing !== void 0 && await gate(existing, "update");
|
|
1166
|
+
existing !== void 0 && await gate(existing, "update"), existing !== void 0 && await guardGate("delete", existing, null);
|
|
1149
1167
|
const transactionId = opts?.transactionId ?? cryptoRandomKey();
|
|
1150
1168
|
let removed = existing;
|
|
1151
1169
|
return opts?.dryRun !== !0 && (removed = store.remove(id), removed !== void 0 && record(transactionId, [{ kind: "delete", id }])), opts?.returnDocuments === void 0 && opts?.returnFirst === void 0 ? removed !== void 0 ? { _id: id } : null : formatMutationResult(
|
|
@@ -1214,14 +1232,19 @@ function makeHandle(backend, projectId, dataset, accessControl, perspective) {
|
|
|
1214
1232
|
backend,
|
|
1215
1233
|
config.projectId ?? projectId,
|
|
1216
1234
|
config.dataset ?? dataset,
|
|
1217
|
-
accessControl,
|
|
1218
|
-
config.perspective ?? perspective
|
|
1235
|
+
config.accessControl ?? accessControl,
|
|
1236
|
+
config.perspective ?? perspective,
|
|
1237
|
+
mutationGuard
|
|
1219
1238
|
),
|
|
1220
1239
|
seed: (documents) => store.seed(documents),
|
|
1221
1240
|
add: (documents) => store.add(documents),
|
|
1222
1241
|
reset: () => store.reset(),
|
|
1223
1242
|
snapshot: () => store.snapshot(),
|
|
1224
1243
|
store: () => store,
|
|
1244
|
+
simulateConcurrentEdit: (id, count = 1) => {
|
|
1245
|
+
const editKey = concurrentEditKey(id);
|
|
1246
|
+
backend.concurrentEdits.set(editKey, (backend.concurrentEdits.get(editKey) ?? 0) + count);
|
|
1247
|
+
},
|
|
1225
1248
|
datasets: () => [...registry.keys()].map(parseRegistryKey),
|
|
1226
1249
|
stub: (name, handler) => {
|
|
1227
1250
|
backend.stubs.set(name, handler);
|