@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/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 = /* @__PURE__ */ new Map();
75
- for (const identity of identities) {
76
- const resolvedId = projectUserOf(identity, pathProjectId).id, existing = byId.get(resolvedId);
77
- if (existing !== void 0 && JSON.stringify(toRecord(existing)) !== JSON.stringify(toRecord(identity)))
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 identity = byId.get(firstId);
86
- if (identity === void 0)
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 toRecord(identity);
109
+ return record;
94
110
  }
95
111
  return ids.flatMap((id) => {
96
- const identity = byId.get(id);
97
- return identity === void 0 ? [] : [toRecord(identity)];
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 ids = /* @__PURE__ */ new Set();
103
- for (const identity of args.identities)
104
- ids.add(projectUserOf(identity, args.pathProjectId).id);
105
- return {
106
- members: [...ids].map((id) => ({
107
- id,
108
- ...id.startsWith("p-") ? { isRobot: !0 } : {}
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"), await hooks?.guard?.("publish", published ?? null, promoted), store.put(promoted), store.remove(action.draftId), { transactionId: cryptoRandomKey() };
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"), await hooks?.guard?.("unpublish", published, null), store.get(action.draftId) === void 0) {
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
- return last;
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 (action, before, after) => {
1178
- mutationGuard !== void 0 && await mutationGuard({ action, before, after, identity: activeAccess()?.currentUser.id });
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 guardGate("update", existing, next), store.put(next), next;
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"), await guardGate("create", null, promoted), store.put(promoted), touchedIds.push(promoted._id), operations.push({ id: promoted._id, operation: "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"), await guardGate(
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"), await guardGate("create", null, promoted), store.put(promoted), touchedIds.push(promoted._id), operations.push({ id: promoted._id, operation: "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"), existing !== void 0 && await guardGate("delete", existing, null), store.remove(mut.id) && (touchedIds.push(mut.id), operations.push({ id: mut.id, operation: "delete" }));
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 guardGate("create", null, promoted);
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 guardGate(existing !== void 0 ? "update" : "create", existing ?? null, promoted);
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 guardGate("create", null, promoted), opts?.dryRun !== !0 && (store.put(promoted), record(transactionId, [{ kind: "createIfNotExists", doc }], opts?.tag)), writeResult(
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 guardGate("delete", existing, null);
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,