@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/README.md
CHANGED
|
@@ -78,7 +78,7 @@ More patterns — passing the fake to code typed against `SanityClient`, asserti
|
|
|
78
78
|
| Actions | `action(action \| action[])` | Content Lake release / version actions, applied atomically |
|
|
79
79
|
| Releases | `releases.*`, `createVersion`, `unpublishVersion` | Content Releases lifecycle — create / edit / schedule / publish / archive / delete, plus version create / replace / discard / unpublish |
|
|
80
80
|
| Config | `config()` | Returns `{ projectId, dataset, perspective }` |
|
|
81
|
-
| Config | `withConfig({ projectId?, dataset?, perspective? })` | Sibling handle into the same multi-dataset registry |
|
|
81
|
+
| Config | `withConfig({ projectId?, dataset?, perspective?, accessControl? })` | Sibling handle into the same multi-dataset registry; `accessControl` rebinds the acting identity (a token-scoped sibling sharing the store) |
|
|
82
82
|
| Perspectives | `raw` / `published` / `drafts` / release stacks | Per-client default or per-`fetch` override; draft/version layering with `_originalId` projection |
|
|
83
83
|
| HTTP | `request({ url? \| uri? })` | Present **only** when the client is created with `accessControl`. Serves `/users/me` + the configured ACL path. Calling it without `accessControl` throws a guidance error. |
|
|
84
84
|
|
|
@@ -96,6 +96,44 @@ More patterns — passing the fake to code typed against `SanityClient`, asserti
|
|
|
96
96
|
> [!NOTE]
|
|
97
97
|
> The static return type of `create` / `delete` reflects the **default** shape (the document / `{ _id } | null`). When you pass `returnDocuments: false` to get an id-shaped result, supply a type argument or cast — the runtime value is correct either way.
|
|
98
98
|
|
|
99
|
+
### Gating writes — `mutationGuard`
|
|
100
|
+
|
|
101
|
+
`createTestClient({ mutationGuard })` installs a pluggable write interceptor. It runs on every write through the document mutation API — `create` / `createOrReplace` / `createIfNotExists` / `patch` / `delete` and the equivalent `transaction()` / `mutate()` operations — _before_ the store changes, and **throws to reject** the write (the throw propagates out of the client call, like a real lake rejection). The release/version action API (`action`, `releases.*`, `createVersion`) is **not** intercepted.
|
|
102
|
+
|
|
103
|
+
The client owns the _seam_; the caller owns the _policy_. This is the hook a workflow test bench uses to enforce Content Lake **mutation guards** at the write path without re-implementing guard evaluation. Each call receives the before/after image (`before` is `null` on create, `after` is `null` on delete), the `action`, and the acting `identity`.
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
const client = createTestClient({
|
|
107
|
+
documents: [{ _id: "article-1", _type: "article", body: "draft" }],
|
|
108
|
+
mutationGuard: ({ action, before, after }) => {
|
|
109
|
+
// Freeze `body` once an article is locked.
|
|
110
|
+
if (action === "update" && before?.locked && before.body !== after?.body) {
|
|
111
|
+
throw new Error("body is locked");
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
await client.patch("article-1").set({ title: "ok" }).commit(); // allowed
|
|
117
|
+
await expect(client.patch("article-1").set({ body: "no" }).commit()).rejects.toThrow();
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
#### Identity is the token
|
|
121
|
+
|
|
122
|
+
`identity` is the id bound to the client's token — its `accessControl.currentUser`. To act as a different user against the **same** store (e.g. to exercise `identity()`-based guards), branch a token-scoped sibling with `withConfig({ accessControl })`:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
const alice = createTestClient({
|
|
126
|
+
documents: [article],
|
|
127
|
+
accessControl: { currentUser: { id: "alice" }, grants },
|
|
128
|
+
mutationGuard: onlyTheAssignedReviewerMayWrite,
|
|
129
|
+
});
|
|
130
|
+
const bob = alice.withConfig({ accessControl: { currentUser: { id: "bob" }, grants } });
|
|
131
|
+
// alice and bob share one store; each write carries its own identity().
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
> [!NOTE]
|
|
135
|
+
> Treat `before` / `after` as read-only — they are the live store images, not copies; mutating them corrupts the store.
|
|
136
|
+
|
|
99
137
|
### Test-only helpers (not on real `SanityClient`)
|
|
100
138
|
|
|
101
139
|
Useful in test bodies, but a giveaway that the code under test isn't using the real client. Keep them in the test file, not in the code being tested.
|
package/dist/index.cjs
CHANGED
|
@@ -876,7 +876,8 @@ function createTestClient(options = {}) {
|
|
|
876
876
|
defaultProjectId,
|
|
877
877
|
defaultDataset,
|
|
878
878
|
options.accessControl,
|
|
879
|
-
options.perspective ?? "raw"
|
|
879
|
+
options.perspective ?? "raw",
|
|
880
|
+
options.mutationGuard
|
|
880
881
|
);
|
|
881
882
|
}
|
|
882
883
|
function dedupeIds(results) {
|
|
@@ -929,7 +930,7 @@ function toTxnMutations(operations) {
|
|
|
929
930
|
}
|
|
930
931
|
];
|
|
931
932
|
}
|
|
932
|
-
function makeHandle(backend, projectId, dataset, accessControl, perspective) {
|
|
933
|
+
function makeHandle(backend, projectId, dataset, accessControl, perspective, mutationGuard) {
|
|
933
934
|
const { registry } = backend, store = (() => {
|
|
934
935
|
const key = registryKey(projectId, dataset);
|
|
935
936
|
let s = registry.get(key);
|
|
@@ -978,6 +979,8 @@ function makeHandle(backend, projectId, dataset, accessControl, perspective) {
|
|
|
978
979
|
permission,
|
|
979
980
|
userId: accessControl.currentUser.id
|
|
980
981
|
});
|
|
982
|
+
}, guardGate = async (action, before, after) => {
|
|
983
|
+
mutationGuard !== void 0 && await mutationGuard({ action, before, after, identity: accessControl?.currentUser.id });
|
|
981
984
|
}, canRead = async (document) => accessControl === void 0 ? !0 : canAccess({
|
|
982
985
|
document,
|
|
983
986
|
grants: accessControl.grants,
|
|
@@ -995,7 +998,7 @@ function makeHandle(backend, projectId, dataset, accessControl, perspective) {
|
|
|
995
998
|
_rev: cryptoRandomKey(),
|
|
996
999
|
_updatedAt: now
|
|
997
1000
|
};
|
|
998
|
-
return store.put(next), next;
|
|
1001
|
+
return await guardGate("update", existing, next), store.put(next), next;
|
|
999
1002
|
}, applyTransaction = async (mutations, txnOptions) => {
|
|
1000
1003
|
const transactionId = txnOptions?.transactionId ?? cryptoRandomKey(), snapshot = /* @__PURE__ */ new Map(), touchedIds = [], operations = [], remember = (id) => {
|
|
1001
1004
|
snapshot.has(id) || snapshot.set(id, store.get(id));
|
|
@@ -1010,13 +1013,17 @@ function makeHandle(backend, projectId, dataset, accessControl, perspective) {
|
|
|
1010
1013
|
throw fakeError(
|
|
1011
1014
|
`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.`
|
|
1012
1015
|
);
|
|
1013
|
-
await gate(promoted, "create"), store.put(promoted), touchedIds.push(promoted._id), operations.push({ id: promoted._id, operation: "create" });
|
|
1016
|
+
await gate(promoted, "create"), await guardGate("create", null, promoted), store.put(promoted), touchedIds.push(promoted._id), operations.push({ id: promoted._id, operation: "create" });
|
|
1014
1017
|
break;
|
|
1015
1018
|
}
|
|
1016
1019
|
case "createOrReplace": {
|
|
1017
1020
|
remember(mut.doc._id);
|
|
1018
1021
|
const promoted = promote2(mut.doc), existing = store.get(promoted._id);
|
|
1019
|
-
await gate(existing ?? promoted, existing !== void 0 ? "update" : "create")
|
|
1022
|
+
await gate(existing ?? promoted, existing !== void 0 ? "update" : "create"), await guardGate(
|
|
1023
|
+
existing !== void 0 ? "update" : "create",
|
|
1024
|
+
existing ?? null,
|
|
1025
|
+
promoted
|
|
1026
|
+
);
|
|
1020
1027
|
const wasPresent = existing !== void 0;
|
|
1021
1028
|
store.put(promoted), touchedIds.push(promoted._id), operations.push({ id: promoted._id, operation: wasPresent ? "update" : "create" });
|
|
1022
1029
|
break;
|
|
@@ -1024,14 +1031,14 @@ function makeHandle(backend, projectId, dataset, accessControl, perspective) {
|
|
|
1024
1031
|
case "createIfNotExists": {
|
|
1025
1032
|
if (remember(mut.doc._id), store.get(mut.doc._id) === void 0) {
|
|
1026
1033
|
const promoted = promote2(mut.doc);
|
|
1027
|
-
await gate(promoted, "create"), store.put(promoted), touchedIds.push(promoted._id), operations.push({ id: promoted._id, operation: "create" });
|
|
1034
|
+
await gate(promoted, "create"), await guardGate("create", null, promoted), store.put(promoted), touchedIds.push(promoted._id), operations.push({ id: promoted._id, operation: "create" });
|
|
1028
1035
|
}
|
|
1029
1036
|
break;
|
|
1030
1037
|
}
|
|
1031
1038
|
case "delete": {
|
|
1032
1039
|
remember(mut.id);
|
|
1033
1040
|
const existing = store.get(mut.id);
|
|
1034
|
-
existing !== void 0 && await gate(existing, "update"), store.remove(mut.id) && (touchedIds.push(mut.id), operations.push({ id: mut.id, operation: "delete" }));
|
|
1041
|
+
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" }));
|
|
1035
1042
|
break;
|
|
1036
1043
|
}
|
|
1037
1044
|
case "patch": {
|
|
@@ -1103,7 +1110,7 @@ function makeHandle(backend, projectId, dataset, accessControl, perspective) {
|
|
|
1103
1110
|
`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.`
|
|
1104
1111
|
);
|
|
1105
1112
|
const promoted = promote2(doc);
|
|
1106
|
-
await gate(promoted, "create");
|
|
1113
|
+
await gate(promoted, "create"), await guardGate("create", null, promoted);
|
|
1107
1114
|
const transactionId = opts?.transactionId ?? cryptoRandomKey();
|
|
1108
1115
|
return opts?.dryRun !== !0 && (store.put(promoted), record(transactionId, [{ kind: "create", doc }])), writeResult(
|
|
1109
1116
|
[promoted],
|
|
@@ -1115,7 +1122,7 @@ function makeHandle(backend, projectId, dataset, accessControl, perspective) {
|
|
|
1115
1122
|
createOrReplace: async (doc, opts) => {
|
|
1116
1123
|
checkRevision(doc._id, opts?.ifRevisionId);
|
|
1117
1124
|
const promoted = promote2(doc), existing = store.get(doc._id);
|
|
1118
|
-
await gate(existing ?? promoted, existing !== void 0 ? "update" : "create");
|
|
1125
|
+
await gate(existing ?? promoted, existing !== void 0 ? "update" : "create"), await guardGate(existing !== void 0 ? "update" : "create", existing ?? null, promoted);
|
|
1119
1126
|
const transactionId = opts?.transactionId ?? cryptoRandomKey();
|
|
1120
1127
|
opts?.dryRun !== !0 && (store.put(promoted), record(transactionId, [{ kind: "createOrReplace", doc }]));
|
|
1121
1128
|
const operation = existing !== void 0 ? "update" : "create";
|
|
@@ -1137,7 +1144,7 @@ function makeHandle(backend, projectId, dataset, accessControl, perspective) {
|
|
|
1137
1144
|
opts
|
|
1138
1145
|
);
|
|
1139
1146
|
const promoted = promote2(doc);
|
|
1140
|
-
return await gate(promoted, "create"), opts?.dryRun !== !0 && (store.put(promoted), record(transactionId, [{ kind: "createIfNotExists", doc }])), writeResult(
|
|
1147
|
+
return await gate(promoted, "create"), await guardGate("create", null, promoted), opts?.dryRun !== !0 && (store.put(promoted), record(transactionId, [{ kind: "createIfNotExists", doc }])), writeResult(
|
|
1141
1148
|
[promoted],
|
|
1142
1149
|
[{ id: promoted._id, operation: "create" }],
|
|
1143
1150
|
transactionId,
|
|
@@ -1147,7 +1154,7 @@ function makeHandle(backend, projectId, dataset, accessControl, perspective) {
|
|
|
1147
1154
|
delete: async (id, opts) => {
|
|
1148
1155
|
checkRevision(id, opts?.ifRevisionId);
|
|
1149
1156
|
const existing = store.get(id);
|
|
1150
|
-
existing !== void 0 && await gate(existing, "update");
|
|
1157
|
+
existing !== void 0 && await gate(existing, "update"), existing !== void 0 && await guardGate("delete", existing, null);
|
|
1151
1158
|
const transactionId = opts?.transactionId ?? cryptoRandomKey();
|
|
1152
1159
|
let removed = existing;
|
|
1153
1160
|
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(
|
|
@@ -1216,8 +1223,9 @@ function makeHandle(backend, projectId, dataset, accessControl, perspective) {
|
|
|
1216
1223
|
backend,
|
|
1217
1224
|
config.projectId ?? projectId,
|
|
1218
1225
|
config.dataset ?? dataset,
|
|
1219
|
-
accessControl,
|
|
1220
|
-
config.perspective ?? perspective
|
|
1226
|
+
config.accessControl ?? accessControl,
|
|
1227
|
+
config.perspective ?? perspective,
|
|
1228
|
+
mutationGuard
|
|
1221
1229
|
),
|
|
1222
1230
|
seed: (documents) => store.seed(documents),
|
|
1223
1231
|
add: (documents) => store.add(documents),
|