@sanity-labs/client-fake-for-test 0.3.0 → 0.4.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 +21 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +67 -0
- package/dist/index.d.ts +67 -0
- package/dist/index.js +21 -13
- 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
|
|
@@ -329,6 +338,58 @@ export declare interface MultipleMutationResult {
|
|
|
329
338
|
}[];
|
|
330
339
|
}
|
|
331
340
|
|
|
341
|
+
/**
|
|
342
|
+
* A pluggable write interceptor, configured via
|
|
343
|
+
* `createTestClient({ mutationGuard })`. It runs on each write through the
|
|
344
|
+
* document mutation API — `create` / `createOrReplace` / `createIfNotExists` /
|
|
345
|
+
* `patch` / `delete` and the equivalent `transaction()` operations — before the
|
|
346
|
+
* store changes; **throw to reject the write** (the throw propagates out of the
|
|
347
|
+
* client call, exactly as a real lake rejection would). The release/version
|
|
348
|
+
* action API (`client.action`, `client.releases.*`, `createVersion`) is **not**
|
|
349
|
+
* intercepted.
|
|
350
|
+
*
|
|
351
|
+
* The client itself has no opinion about *what* a guard is — it only provides
|
|
352
|
+
* the seam and the before/after/identity context. Callers (e.g. a workflow test
|
|
353
|
+
* bench) supply the policy: evaluate lake mutation guards, enforce field locks,
|
|
354
|
+
* etc. Keeping the policy in the caller avoids duplicating guard-evaluation
|
|
355
|
+
* logic the client has no business owning.
|
|
356
|
+
*/
|
|
357
|
+
export declare type MutationGuard = (
|
|
358
|
+
context: MutationGuardContext,
|
|
359
|
+
) => void | Promise<void>;
|
|
360
|
+
|
|
361
|
+
/** The kind of write a {@link MutationGuard} is asked to vet. */
|
|
362
|
+
export declare type MutationGuardAction = "create" | "update" | "delete";
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* The before/after image of a single document write, handed to a
|
|
366
|
+
* {@link MutationGuard} before it is applied.
|
|
367
|
+
*
|
|
368
|
+
* - `before` is null on create; `after` is null on delete.
|
|
369
|
+
* - `identity` is the acting user — the id bound to the client's token
|
|
370
|
+
* (its `accessControl.currentUser`), or undefined when the client has no
|
|
371
|
+
* identity configured.
|
|
372
|
+
*
|
|
373
|
+
* Treat `before`/`after` as read-only — they are the live store images, not
|
|
374
|
+
* copies; a guard that mutates them corrupts the store.
|
|
375
|
+
*/
|
|
376
|
+
export declare interface MutationGuardContext {
|
|
377
|
+
action: MutationGuardAction;
|
|
378
|
+
before:
|
|
379
|
+
| (SanityDocumentLike & {
|
|
380
|
+
_id: string;
|
|
381
|
+
_type: string;
|
|
382
|
+
})
|
|
383
|
+
| null;
|
|
384
|
+
after:
|
|
385
|
+
| (SanityDocumentLike & {
|
|
386
|
+
_id: string;
|
|
387
|
+
_type: string;
|
|
388
|
+
})
|
|
389
|
+
| null;
|
|
390
|
+
identity: string | undefined;
|
|
391
|
+
}
|
|
392
|
+
|
|
332
393
|
/** Append-only record of one committed transaction (a single mutation is a 1-entry transaction). */
|
|
333
394
|
export declare interface MutationLogEntry {
|
|
334
395
|
transactionId: string;
|
|
@@ -706,6 +767,12 @@ export declare interface TestClient {
|
|
|
706
767
|
projectId: string;
|
|
707
768
|
dataset: string;
|
|
708
769
|
perspective: Perspective;
|
|
770
|
+
/**
|
|
771
|
+
* Rebind the acting identity (and its grants) on a sibling that shares
|
|
772
|
+
* the same store — the test analogue of a token-scoped client. Lets two
|
|
773
|
+
* actors write the same dataset, e.g. for `identity()`-based guards.
|
|
774
|
+
*/
|
|
775
|
+
accessControl: TestClientAccessControl;
|
|
709
776
|
}>,
|
|
710
777
|
) => TestClient;
|
|
711
778
|
/** Replace the current dataset with the given documents. */
|
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
|
|
@@ -329,6 +338,58 @@ export declare interface MultipleMutationResult {
|
|
|
329
338
|
}[];
|
|
330
339
|
}
|
|
331
340
|
|
|
341
|
+
/**
|
|
342
|
+
* A pluggable write interceptor, configured via
|
|
343
|
+
* `createTestClient({ mutationGuard })`. It runs on each write through the
|
|
344
|
+
* document mutation API — `create` / `createOrReplace` / `createIfNotExists` /
|
|
345
|
+
* `patch` / `delete` and the equivalent `transaction()` operations — before the
|
|
346
|
+
* store changes; **throw to reject the write** (the throw propagates out of the
|
|
347
|
+
* client call, exactly as a real lake rejection would). The release/version
|
|
348
|
+
* action API (`client.action`, `client.releases.*`, `createVersion`) is **not**
|
|
349
|
+
* intercepted.
|
|
350
|
+
*
|
|
351
|
+
* The client itself has no opinion about *what* a guard is — it only provides
|
|
352
|
+
* the seam and the before/after/identity context. Callers (e.g. a workflow test
|
|
353
|
+
* bench) supply the policy: evaluate lake mutation guards, enforce field locks,
|
|
354
|
+
* etc. Keeping the policy in the caller avoids duplicating guard-evaluation
|
|
355
|
+
* logic the client has no business owning.
|
|
356
|
+
*/
|
|
357
|
+
export declare type MutationGuard = (
|
|
358
|
+
context: MutationGuardContext,
|
|
359
|
+
) => void | Promise<void>;
|
|
360
|
+
|
|
361
|
+
/** The kind of write a {@link MutationGuard} is asked to vet. */
|
|
362
|
+
export declare type MutationGuardAction = "create" | "update" | "delete";
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* The before/after image of a single document write, handed to a
|
|
366
|
+
* {@link MutationGuard} before it is applied.
|
|
367
|
+
*
|
|
368
|
+
* - `before` is null on create; `after` is null on delete.
|
|
369
|
+
* - `identity` is the acting user — the id bound to the client's token
|
|
370
|
+
* (its `accessControl.currentUser`), or undefined when the client has no
|
|
371
|
+
* identity configured.
|
|
372
|
+
*
|
|
373
|
+
* Treat `before`/`after` as read-only — they are the live store images, not
|
|
374
|
+
* copies; a guard that mutates them corrupts the store.
|
|
375
|
+
*/
|
|
376
|
+
export declare interface MutationGuardContext {
|
|
377
|
+
action: MutationGuardAction;
|
|
378
|
+
before:
|
|
379
|
+
| (SanityDocumentLike & {
|
|
380
|
+
_id: string;
|
|
381
|
+
_type: string;
|
|
382
|
+
})
|
|
383
|
+
| null;
|
|
384
|
+
after:
|
|
385
|
+
| (SanityDocumentLike & {
|
|
386
|
+
_id: string;
|
|
387
|
+
_type: string;
|
|
388
|
+
})
|
|
389
|
+
| null;
|
|
390
|
+
identity: string | undefined;
|
|
391
|
+
}
|
|
392
|
+
|
|
332
393
|
/** Append-only record of one committed transaction (a single mutation is a 1-entry transaction). */
|
|
333
394
|
export declare interface MutationLogEntry {
|
|
334
395
|
transactionId: string;
|
|
@@ -706,6 +767,12 @@ export declare interface TestClient {
|
|
|
706
767
|
projectId: string;
|
|
707
768
|
dataset: string;
|
|
708
769
|
perspective: Perspective;
|
|
770
|
+
/**
|
|
771
|
+
* Rebind the acting identity (and its grants) on a sibling that shares
|
|
772
|
+
* the same store — the test analogue of a token-scoped client. Lets two
|
|
773
|
+
* actors write the same dataset, e.g. for `identity()`-based guards.
|
|
774
|
+
*/
|
|
775
|
+
accessControl: TestClientAccessControl;
|
|
709
776
|
}>,
|
|
710
777
|
) => TestClient;
|
|
711
778
|
/** Replace the current dataset with the given documents. */
|
package/dist/index.js
CHANGED
|
@@ -874,7 +874,8 @@ function createTestClient(options = {}) {
|
|
|
874
874
|
defaultProjectId,
|
|
875
875
|
defaultDataset,
|
|
876
876
|
options.accessControl,
|
|
877
|
-
options.perspective ?? "raw"
|
|
877
|
+
options.perspective ?? "raw",
|
|
878
|
+
options.mutationGuard
|
|
878
879
|
);
|
|
879
880
|
}
|
|
880
881
|
function dedupeIds(results) {
|
|
@@ -927,7 +928,7 @@ function toTxnMutations(operations) {
|
|
|
927
928
|
}
|
|
928
929
|
];
|
|
929
930
|
}
|
|
930
|
-
function makeHandle(backend, projectId, dataset, accessControl, perspective) {
|
|
931
|
+
function makeHandle(backend, projectId, dataset, accessControl, perspective, mutationGuard) {
|
|
931
932
|
const { registry } = backend, store = (() => {
|
|
932
933
|
const key = registryKey(projectId, dataset);
|
|
933
934
|
let s = registry.get(key);
|
|
@@ -976,6 +977,8 @@ function makeHandle(backend, projectId, dataset, accessControl, perspective) {
|
|
|
976
977
|
permission,
|
|
977
978
|
userId: accessControl.currentUser.id
|
|
978
979
|
});
|
|
980
|
+
}, guardGate = async (action, before, after) => {
|
|
981
|
+
mutationGuard !== void 0 && await mutationGuard({ action, before, after, identity: accessControl?.currentUser.id });
|
|
979
982
|
}, canRead = async (document) => accessControl === void 0 ? !0 : canAccess({
|
|
980
983
|
document,
|
|
981
984
|
grants: accessControl.grants,
|
|
@@ -993,7 +996,7 @@ function makeHandle(backend, projectId, dataset, accessControl, perspective) {
|
|
|
993
996
|
_rev: cryptoRandomKey(),
|
|
994
997
|
_updatedAt: now
|
|
995
998
|
};
|
|
996
|
-
return store.put(next), next;
|
|
999
|
+
return await guardGate("update", existing, next), store.put(next), next;
|
|
997
1000
|
}, applyTransaction = async (mutations, txnOptions) => {
|
|
998
1001
|
const transactionId = txnOptions?.transactionId ?? cryptoRandomKey(), snapshot = /* @__PURE__ */ new Map(), touchedIds = [], operations = [], remember = (id) => {
|
|
999
1002
|
snapshot.has(id) || snapshot.set(id, store.get(id));
|
|
@@ -1008,13 +1011,17 @@ function makeHandle(backend, projectId, dataset, accessControl, perspective) {
|
|
|
1008
1011
|
throw fakeError(
|
|
1009
1012
|
`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
1013
|
);
|
|
1011
|
-
await gate(promoted, "create"), store.put(promoted), touchedIds.push(promoted._id), operations.push({ id: promoted._id, operation: "create" });
|
|
1014
|
+
await gate(promoted, "create"), await guardGate("create", null, promoted), store.put(promoted), touchedIds.push(promoted._id), operations.push({ id: promoted._id, operation: "create" });
|
|
1012
1015
|
break;
|
|
1013
1016
|
}
|
|
1014
1017
|
case "createOrReplace": {
|
|
1015
1018
|
remember(mut.doc._id);
|
|
1016
1019
|
const promoted = promote2(mut.doc), existing = store.get(promoted._id);
|
|
1017
|
-
await gate(existing ?? promoted, existing !== void 0 ? "update" : "create")
|
|
1020
|
+
await gate(existing ?? promoted, existing !== void 0 ? "update" : "create"), await guardGate(
|
|
1021
|
+
existing !== void 0 ? "update" : "create",
|
|
1022
|
+
existing ?? null,
|
|
1023
|
+
promoted
|
|
1024
|
+
);
|
|
1018
1025
|
const wasPresent = existing !== void 0;
|
|
1019
1026
|
store.put(promoted), touchedIds.push(promoted._id), operations.push({ id: promoted._id, operation: wasPresent ? "update" : "create" });
|
|
1020
1027
|
break;
|
|
@@ -1022,14 +1029,14 @@ function makeHandle(backend, projectId, dataset, accessControl, perspective) {
|
|
|
1022
1029
|
case "createIfNotExists": {
|
|
1023
1030
|
if (remember(mut.doc._id), store.get(mut.doc._id) === void 0) {
|
|
1024
1031
|
const promoted = promote2(mut.doc);
|
|
1025
|
-
await gate(promoted, "create"), store.put(promoted), touchedIds.push(promoted._id), operations.push({ id: promoted._id, operation: "create" });
|
|
1032
|
+
await gate(promoted, "create"), await guardGate("create", null, promoted), store.put(promoted), touchedIds.push(promoted._id), operations.push({ id: promoted._id, operation: "create" });
|
|
1026
1033
|
}
|
|
1027
1034
|
break;
|
|
1028
1035
|
}
|
|
1029
1036
|
case "delete": {
|
|
1030
1037
|
remember(mut.id);
|
|
1031
1038
|
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" }));
|
|
1039
|
+
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
1040
|
break;
|
|
1034
1041
|
}
|
|
1035
1042
|
case "patch": {
|
|
@@ -1101,7 +1108,7 @@ function makeHandle(backend, projectId, dataset, accessControl, perspective) {
|
|
|
1101
1108
|
`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
1109
|
);
|
|
1103
1110
|
const promoted = promote2(doc);
|
|
1104
|
-
await gate(promoted, "create");
|
|
1111
|
+
await gate(promoted, "create"), await guardGate("create", null, promoted);
|
|
1105
1112
|
const transactionId = opts?.transactionId ?? cryptoRandomKey();
|
|
1106
1113
|
return opts?.dryRun !== !0 && (store.put(promoted), record(transactionId, [{ kind: "create", doc }])), writeResult(
|
|
1107
1114
|
[promoted],
|
|
@@ -1113,7 +1120,7 @@ function makeHandle(backend, projectId, dataset, accessControl, perspective) {
|
|
|
1113
1120
|
createOrReplace: async (doc, opts) => {
|
|
1114
1121
|
checkRevision(doc._id, opts?.ifRevisionId);
|
|
1115
1122
|
const promoted = promote2(doc), existing = store.get(doc._id);
|
|
1116
|
-
await gate(existing ?? promoted, existing !== void 0 ? "update" : "create");
|
|
1123
|
+
await gate(existing ?? promoted, existing !== void 0 ? "update" : "create"), await guardGate(existing !== void 0 ? "update" : "create", existing ?? null, promoted);
|
|
1117
1124
|
const transactionId = opts?.transactionId ?? cryptoRandomKey();
|
|
1118
1125
|
opts?.dryRun !== !0 && (store.put(promoted), record(transactionId, [{ kind: "createOrReplace", doc }]));
|
|
1119
1126
|
const operation = existing !== void 0 ? "update" : "create";
|
|
@@ -1135,7 +1142,7 @@ function makeHandle(backend, projectId, dataset, accessControl, perspective) {
|
|
|
1135
1142
|
opts
|
|
1136
1143
|
);
|
|
1137
1144
|
const promoted = promote2(doc);
|
|
1138
|
-
return await gate(promoted, "create"), opts?.dryRun !== !0 && (store.put(promoted), record(transactionId, [{ kind: "createIfNotExists", doc }])), writeResult(
|
|
1145
|
+
return await gate(promoted, "create"), await guardGate("create", null, promoted), opts?.dryRun !== !0 && (store.put(promoted), record(transactionId, [{ kind: "createIfNotExists", doc }])), writeResult(
|
|
1139
1146
|
[promoted],
|
|
1140
1147
|
[{ id: promoted._id, operation: "create" }],
|
|
1141
1148
|
transactionId,
|
|
@@ -1145,7 +1152,7 @@ function makeHandle(backend, projectId, dataset, accessControl, perspective) {
|
|
|
1145
1152
|
delete: async (id, opts) => {
|
|
1146
1153
|
checkRevision(id, opts?.ifRevisionId);
|
|
1147
1154
|
const existing = store.get(id);
|
|
1148
|
-
existing !== void 0 && await gate(existing, "update");
|
|
1155
|
+
existing !== void 0 && await gate(existing, "update"), existing !== void 0 && await guardGate("delete", existing, null);
|
|
1149
1156
|
const transactionId = opts?.transactionId ?? cryptoRandomKey();
|
|
1150
1157
|
let removed = existing;
|
|
1151
1158
|
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,8 +1221,9 @@ function makeHandle(backend, projectId, dataset, accessControl, perspective) {
|
|
|
1214
1221
|
backend,
|
|
1215
1222
|
config.projectId ?? projectId,
|
|
1216
1223
|
config.dataset ?? dataset,
|
|
1217
|
-
accessControl,
|
|
1218
|
-
config.perspective ?? perspective
|
|
1224
|
+
config.accessControl ?? accessControl,
|
|
1225
|
+
config.perspective ?? perspective,
|
|
1226
|
+
mutationGuard
|
|
1219
1227
|
),
|
|
1220
1228
|
seed: (documents) => store.seed(documents),
|
|
1221
1229
|
add: (documents) => store.add(documents),
|