@sanity-labs/client-fake-for-test 0.10.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 +14 -5
- package/dist/index.cjs +117 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +70 -60
- package/dist/index.d.ts +70 -60
- package/dist/index.js +117 -47
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -62,6 +62,28 @@ function defaultAclPath(projectId, dataset) {
|
|
|
62
62
|
function projectUserOf(identity, projectId) {
|
|
63
63
|
return identity.usersByProject?.[projectId] ?? identity.user;
|
|
64
64
|
}
|
|
65
|
+
function consistentProjectRecords(args) {
|
|
66
|
+
const byId = /* @__PURE__ */ new Map();
|
|
67
|
+
for (const record of args.records) {
|
|
68
|
+
const existing = byId.get(record.id);
|
|
69
|
+
if (existing !== void 0 && JSON.stringify(existing) !== JSON.stringify(record))
|
|
70
|
+
throw fakeError(
|
|
71
|
+
`request(): two registered identities resolve to project user "${record.id}" in project "${args.projectId}" with different records. The project records cannot be served unambiguously \u2014 fix the identity registrations so duplicate ids carry identical records.`
|
|
72
|
+
);
|
|
73
|
+
byId.set(record.id, record);
|
|
74
|
+
}
|
|
75
|
+
return byId;
|
|
76
|
+
}
|
|
77
|
+
function canonicalProjectRoles(roles) {
|
|
78
|
+
return roles.map((role) => ({
|
|
79
|
+
name: role.name,
|
|
80
|
+
...role.title !== void 0 ? { title: role.title } : {},
|
|
81
|
+
...role.description !== void 0 ? { description: role.description } : {}
|
|
82
|
+
})).toSorted((left, right) => {
|
|
83
|
+
const leftKey = JSON.stringify(left), rightKey = JSON.stringify(right);
|
|
84
|
+
return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
|
|
85
|
+
});
|
|
86
|
+
}
|
|
65
87
|
function serveProjectUsers(args) {
|
|
66
88
|
const { requestedIds, pathProjectId, identities } = args, toRecord = (identity) => {
|
|
67
89
|
const projectUser = projectUserOf(identity, pathProjectId);
|
|
@@ -71,43 +93,37 @@ function serveProjectUsers(args) {
|
|
|
71
93
|
displayName: projectUser.name ?? projectUser.id,
|
|
72
94
|
...projectUser.email !== void 0 ? { email: projectUser.email } : {}
|
|
73
95
|
};
|
|
74
|
-
}, byId =
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
throw fakeError(
|
|
79
|
-
`request(): two registered identities resolve to project user "${resolvedId}" in project "${pathProjectId}" with different records. The directory cannot answer unambiguously \u2014 fix the identity registrations so duplicate ids carry identical records.`
|
|
80
|
-
);
|
|
81
|
-
byId.set(resolvedId, identity);
|
|
82
|
-
}
|
|
83
|
-
const ids = requestedIds.split(","), [firstId] = ids;
|
|
96
|
+
}, byId = consistentProjectRecords({
|
|
97
|
+
projectId: pathProjectId,
|
|
98
|
+
records: identities.map(toRecord)
|
|
99
|
+
}), ids = requestedIds.split(","), [firstId] = ids;
|
|
84
100
|
if (ids.length === 1 && firstId !== void 0) {
|
|
85
|
-
const
|
|
86
|
-
if (
|
|
101
|
+
const record = byId.get(firstId);
|
|
102
|
+
if (record === void 0)
|
|
87
103
|
throw Object.assign(
|
|
88
104
|
fakeError(
|
|
89
105
|
`request(): no registered identity resolves to project user "${firstId}" in project "${pathProjectId}". Register it via createTestClient({ identities }) or addIdentity().`
|
|
90
106
|
),
|
|
91
107
|
{ statusCode: 404 }
|
|
92
108
|
);
|
|
93
|
-
return
|
|
109
|
+
return record;
|
|
94
110
|
}
|
|
95
111
|
return ids.flatMap((id) => {
|
|
96
|
-
const
|
|
97
|
-
return
|
|
112
|
+
const record = byId.get(id);
|
|
113
|
+
return record === void 0 ? [] : [record];
|
|
98
114
|
});
|
|
99
115
|
}
|
|
100
116
|
const PROJECT_USERS_PATH = /^\/projects\/([^/]+)\/users\/([^/]+)$/, PROJECT_PATH = /^\/projects\/([^/]+)$/;
|
|
101
117
|
function serveProject(args) {
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
};
|
|
118
|
+
const records = args.identities.map((identity) => {
|
|
119
|
+
const projectUser = projectUserOf(identity, args.pathProjectId);
|
|
120
|
+
return {
|
|
121
|
+
id: projectUser.id,
|
|
122
|
+
...projectUser.id.startsWith("p-") ? { isRobot: !0 } : {},
|
|
123
|
+
...projectUser.roles !== void 0 ? { roles: canonicalProjectRoles(projectUser.roles) } : {}
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
return { members: [...consistentProjectRecords({ projectId: args.pathProjectId, records }).values()] };
|
|
111
127
|
}
|
|
112
128
|
function makeRequestImpl(ctx) {
|
|
113
129
|
const { getAccess, projectId, dataset, useProjectHostname, listIdentities } = ctx;
|
|
@@ -135,6 +151,48 @@ function makeRequestImpl(ctx) {
|
|
|
135
151
|
);
|
|
136
152
|
};
|
|
137
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
|
+
}
|
|
138
196
|
function describeType(value) {
|
|
139
197
|
return value === null ? "null" : Array.isArray(value) ? "an array" : `a ${typeof value}`;
|
|
140
198
|
}
|
|
@@ -668,7 +726,7 @@ async function applyAction(store, action, hooks) {
|
|
|
668
726
|
const published = store.get(action.publishedId);
|
|
669
727
|
requireRevisionMatch(action.draftId, draft._rev, action.ifDraftRevisionId), requireRevisionMatch(action.publishedId, published?._rev, action.ifPublishedRevisionId);
|
|
670
728
|
const promoted = promote({ ...draft, _id: action.publishedId });
|
|
671
|
-
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() };
|
|
672
730
|
}
|
|
673
731
|
case "sanity.action.document.unpublish": {
|
|
674
732
|
requireDraftOf(action);
|
|
@@ -677,7 +735,7 @@ async function applyAction(store, action, hooks) {
|
|
|
677
735
|
throw fakeError(
|
|
678
736
|
`Cannot unpublish "${action.publishedId}": no published document with that _id exists. document.unpublish retracts a live published document into "${action.draftId}".`
|
|
679
737
|
);
|
|
680
|
-
if (await hooks?.gate?.(published, "update"),
|
|
738
|
+
if (await hooks?.gate?.(published, "update"), store.get(action.draftId) === void 0) {
|
|
681
739
|
const draft = promote({ ...published, _id: action.draftId });
|
|
682
740
|
draft._createdAt = published._createdAt, store.put(draft);
|
|
683
741
|
}
|
|
@@ -698,7 +756,7 @@ function requireRevisionMatch(documentId, actualRev, expectedRev) {
|
|
|
698
756
|
);
|
|
699
757
|
}
|
|
700
758
|
async function applyActions(store, actions, hooks) {
|
|
701
|
-
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) => {
|
|
702
760
|
snapshot.has(id) || snapshot.set(id, store.get(id));
|
|
703
761
|
};
|
|
704
762
|
try {
|
|
@@ -734,13 +792,18 @@ async function applyActions(store, actions, hooks) {
|
|
|
734
792
|
remember(a.versionId);
|
|
735
793
|
break;
|
|
736
794
|
case "sanity.action.document.publish":
|
|
795
|
+
remember(a.draftId), remember(a.publishedId), guardedIds.add(a.publishedId), guardedIds.add(a.draftId);
|
|
796
|
+
break;
|
|
737
797
|
case "sanity.action.document.unpublish":
|
|
738
|
-
remember(a.draftId), remember(a.publishedId);
|
|
798
|
+
remember(a.draftId), remember(a.publishedId), guardedIds.add(a.draftId), guardedIds.add(a.publishedId);
|
|
739
799
|
break;
|
|
740
800
|
}
|
|
741
801
|
last = await applyAction(store, a, hooks);
|
|
742
802
|
}
|
|
743
|
-
|
|
803
|
+
const contexts = mutationContexts(
|
|
804
|
+
[...guardedIds].map((id) => ({ before: snapshot.get(id), after: store.get(id) }))
|
|
805
|
+
);
|
|
806
|
+
return await hooks?.guard?.(contexts), last;
|
|
744
807
|
} catch (err) {
|
|
745
808
|
for (const [id, doc] of snapshot)
|
|
746
809
|
doc === void 0 ? store.remove(id) : store.put(doc);
|
|
@@ -1171,11 +1234,17 @@ function makeHandle(handleArgs) {
|
|
|
1171
1234
|
access !== void 0 && await enforceWriteAccess({
|
|
1172
1235
|
document,
|
|
1173
1236
|
grants: access.grants,
|
|
1174
|
-
permission,
|
|
1237
|
+
permission: document._id !== void 0 && isSystemId(document._id) ? "manage" : permission,
|
|
1175
1238
|
userId: access.currentUser.id
|
|
1176
1239
|
});
|
|
1177
|
-
}, guardGate = async (
|
|
1178
|
-
|
|
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 }]);
|
|
1179
1248
|
}, canRead = async (document) => {
|
|
1180
1249
|
const access = activeAccess();
|
|
1181
1250
|
return access === void 0 ? !0 : canAccess({
|
|
@@ -1184,7 +1253,7 @@ function makeHandle(handleArgs) {
|
|
|
1184
1253
|
permission: "read",
|
|
1185
1254
|
userId: access.currentUser.id
|
|
1186
1255
|
});
|
|
1187
|
-
}, applyPatch = async (documentId, ops, ifRev) => {
|
|
1256
|
+
}, applyPatch = async (documentId, ops, ifRev, options = {}) => {
|
|
1188
1257
|
const existing = store.get(documentId);
|
|
1189
1258
|
if (existing === void 0)
|
|
1190
1259
|
throw fakeError(
|
|
@@ -1196,7 +1265,7 @@ function makeHandle(handleArgs) {
|
|
|
1196
1265
|
_rev: cryptoRandomKey(),
|
|
1197
1266
|
_updatedAt: now
|
|
1198
1267
|
};
|
|
1199
|
-
return await
|
|
1268
|
+
return options.evaluateGuard !== !1 && await guardOne("update", existing, next), store.put(next), next;
|
|
1200
1269
|
}, applyTransaction = async (mutations, txnOptions) => {
|
|
1201
1270
|
const transactionId = txnOptions?.transactionId ?? cryptoRandomKey(), snapshot = /* @__PURE__ */ new Map(), touchedIds = [], operations = [], remember = (id) => {
|
|
1202
1271
|
snapshot.has(id) || snapshot.set(id, store.get(id));
|
|
@@ -1211,17 +1280,13 @@ function makeHandle(handleArgs) {
|
|
|
1211
1280
|
throw fakeError(
|
|
1212
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.`
|
|
1213
1282
|
);
|
|
1214
|
-
await gate(promoted, "create"),
|
|
1283
|
+
await gate(promoted, "create"), store.put(promoted), touchedIds.push(promoted._id), operations.push({ id: promoted._id, operation: "create" });
|
|
1215
1284
|
break;
|
|
1216
1285
|
}
|
|
1217
1286
|
case "createOrReplace": {
|
|
1218
1287
|
remember(mut.doc._id);
|
|
1219
1288
|
const promoted = promote2(mut.doc), existing = store.get(promoted._id);
|
|
1220
|
-
await gate(existing ?? promoted, existing !== void 0 ? "update" : "create")
|
|
1221
|
-
existing !== void 0 ? "update" : "create",
|
|
1222
|
-
existing ?? null,
|
|
1223
|
-
promoted
|
|
1224
|
-
);
|
|
1289
|
+
await gate(existing ?? promoted, existing !== void 0 ? "update" : "create");
|
|
1225
1290
|
const wasPresent = existing !== void 0;
|
|
1226
1291
|
store.put(promoted), touchedIds.push(promoted._id), operations.push({ id: promoted._id, operation: wasPresent ? "update" : "create" });
|
|
1227
1292
|
break;
|
|
@@ -1229,21 +1294,25 @@ function makeHandle(handleArgs) {
|
|
|
1229
1294
|
case "createIfNotExists": {
|
|
1230
1295
|
if (remember(mut.doc._id), store.get(mut.doc._id) === void 0) {
|
|
1231
1296
|
const promoted = promote2(mut.doc);
|
|
1232
|
-
await gate(promoted, "create"),
|
|
1297
|
+
await gate(promoted, "create"), store.put(promoted), touchedIds.push(promoted._id), operations.push({ id: promoted._id, operation: "create" });
|
|
1233
1298
|
}
|
|
1234
1299
|
break;
|
|
1235
1300
|
}
|
|
1236
1301
|
case "delete": {
|
|
1237
1302
|
remember(mut.id);
|
|
1238
1303
|
const existing = store.get(mut.id);
|
|
1239
|
-
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" }));
|
|
1240
1305
|
break;
|
|
1241
1306
|
}
|
|
1242
1307
|
case "patch": {
|
|
1243
|
-
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" });
|
|
1244
1309
|
break;
|
|
1245
1310
|
}
|
|
1246
1311
|
}
|
|
1312
|
+
const contexts = mutationContexts(
|
|
1313
|
+
[...snapshot].map(([id, before]) => ({ before, after: store.get(id) }))
|
|
1314
|
+
);
|
|
1315
|
+
await guardGate(contexts);
|
|
1247
1316
|
} catch (error) {
|
|
1248
1317
|
for (const [id, doc] of snapshot)
|
|
1249
1318
|
doc === void 0 ? store.remove(id) : store.put(doc);
|
|
@@ -1308,7 +1377,7 @@ function makeHandle(handleArgs) {
|
|
|
1308
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.`
|
|
1309
1378
|
);
|
|
1310
1379
|
const promoted = promote2(doc);
|
|
1311
|
-
await gate(promoted, "create"), await
|
|
1380
|
+
await gate(promoted, "create"), await guardOne("create", null, promoted);
|
|
1312
1381
|
const transactionId = opts?.transactionId ?? cryptoRandomKey();
|
|
1313
1382
|
return opts?.dryRun !== !0 && (store.put(promoted), record(transactionId, [{ kind: "create", doc }], opts?.tag)), writeResult(
|
|
1314
1383
|
[promoted],
|
|
@@ -1320,7 +1389,7 @@ function makeHandle(handleArgs) {
|
|
|
1320
1389
|
createOrReplace: (doc, opts) => serializeWrite(backend, async () => {
|
|
1321
1390
|
checkRevision(doc._id, opts?.ifRevisionId);
|
|
1322
1391
|
const promoted = promote2(doc), existing = store.get(doc._id);
|
|
1323
|
-
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);
|
|
1324
1393
|
const transactionId = opts?.transactionId ?? cryptoRandomKey();
|
|
1325
1394
|
opts?.dryRun !== !0 && (store.put(promoted), record(transactionId, [{ kind: "createOrReplace", doc }], opts?.tag));
|
|
1326
1395
|
const operation = existing !== void 0 ? "update" : "create";
|
|
@@ -1342,7 +1411,7 @@ function makeHandle(handleArgs) {
|
|
|
1342
1411
|
opts
|
|
1343
1412
|
);
|
|
1344
1413
|
const promoted = promote2(doc);
|
|
1345
|
-
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(
|
|
1346
1415
|
[promoted],
|
|
1347
1416
|
[{ id: promoted._id, operation: "create" }],
|
|
1348
1417
|
transactionId,
|
|
@@ -1352,7 +1421,7 @@ function makeHandle(handleArgs) {
|
|
|
1352
1421
|
delete: (id, opts) => serializeWrite(backend, async () => {
|
|
1353
1422
|
checkRevision(id, opts?.ifRevisionId);
|
|
1354
1423
|
const existing = store.get(id);
|
|
1355
|
-
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);
|
|
1356
1425
|
const transactionId = opts?.transactionId ?? cryptoRandomKey();
|
|
1357
1426
|
let removed = existing;
|
|
1358
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(
|
|
@@ -1503,6 +1572,7 @@ function makeHandle(handleArgs) {
|
|
|
1503
1572
|
}
|
|
1504
1573
|
export {
|
|
1505
1574
|
ERR_PREFIX,
|
|
1575
|
+
GuardDeniedError,
|
|
1506
1576
|
SUPPORTED_SURFACE,
|
|
1507
1577
|
UNSUPPORTED_MEMBERS,
|
|
1508
1578
|
WriteAccessDeniedError,
|