@sanity-labs/client-fake-for-test 0.7.1 → 0.9.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
@@ -59,69 +59,65 @@ async function enforceWriteAccess(args) {
59
59
  function defaultAclPath(projectId, dataset) {
60
60
  return `/projects/${projectId}/datasets/${dataset}/acl`;
61
61
  }
62
- function makeRequestImpl(getAccess, projectId, dataset) {
62
+ function projectUserOf(identity, projectId) {
63
+ return identity.usersByProject?.[projectId] ?? identity.user;
64
+ }
65
+ function serveProjectUsers(args) {
66
+ const { requestedIds, pathProjectId, identities } = args, toRecord = (identity) => {
67
+ const projectUser = projectUserOf(identity, pathProjectId);
68
+ return {
69
+ id: projectUser.id,
70
+ sanityUserId: identity.globalUser?.id ?? projectUser.id,
71
+ displayName: projectUser.name ?? projectUser.id,
72
+ ...projectUser.email !== void 0 ? { email: projectUser.email } : {}
73
+ };
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;
84
+ if (ids.length === 1 && firstId !== void 0) {
85
+ const identity = byId.get(firstId);
86
+ if (identity === void 0)
87
+ throw Object.assign(
88
+ fakeError(
89
+ `request(): no registered identity resolves to project user "${firstId}" in project "${pathProjectId}". Register it via createTestClient({ identities }) or addIdentity().`
90
+ ),
91
+ { statusCode: 404 }
92
+ );
93
+ return toRecord(identity);
94
+ }
95
+ return ids.flatMap((id) => {
96
+ const identity = byId.get(id);
97
+ return identity === void 0 ? [] : [toRecord(identity)];
98
+ });
99
+ }
100
+ const PROJECT_USERS_PATH = /^\/projects\/([^/]+)\/users\/([^/]+)$/;
101
+ function makeRequestImpl(ctx) {
102
+ const { getAccess, projectId, dataset, useProjectHostname, listIdentities } = ctx;
63
103
  return async (opts) => {
64
104
  const accessControl = getAccess(), aclPath = accessControl.aclPath ?? defaultAclPath(projectId, dataset), path = opts.uri ?? opts.url;
65
105
  if (path === "/users/me")
66
- return accessControl.currentUser;
106
+ return useProjectHostname ? accessControl.currentUser : accessControl.globalUser ?? accessControl.currentUser;
67
107
  if (path === aclPath)
68
108
  return accessControl.grants;
109
+ const projectUsers = path?.match(PROJECT_USERS_PATH), [, pathProjectId, requestedIds] = projectUsers ?? [];
110
+ if (pathProjectId !== void 0 && requestedIds !== void 0)
111
+ return serveProjectUsers({
112
+ requestedIds,
113
+ pathProjectId,
114
+ identities: listIdentities()
115
+ });
69
116
  throw fakeError(
70
- `request(): no handler for path "${path}". With access control configured, the fake only serves "/users/me" and "${aclPath}". For any other endpoint, register a stub (client.stub("request", fn)) or intercept the HTTP call with a tool like MSW.`
117
+ `request(): no handler for path "${path}". With access control configured, the fake serves "/users/me", "${aclPath}", and "/projects/<projectId>/users/<ids>". For any other endpoint, register a stub (client.stub("request", fn)) or intercept the HTTP call with a tool like MSW.`
71
118
  );
72
119
  };
73
120
  }
74
- function createTestStore(initial) {
75
- const docs = /* @__PURE__ */ new Map(), promote2 = (doc) => {
76
- if (typeof doc._id != "string" || doc._id.length === 0)
77
- throw fakeError(
78
- `Cannot seed/store a document without a non-empty string \`_id\` (got ${JSON.stringify(doc._id)}). This fake never auto-generates ids \u2014 give every document an explicit _id. Document: ${JSON.stringify(doc)}`
79
- );
80
- if (typeof doc._type != "string" || doc._type.length === 0)
81
- throw fakeError(
82
- `Document "${doc._id}" is missing a non-empty string \`_type\` (got ${JSON.stringify(doc._type)}). Every Sanity document needs a _type.`
83
- );
84
- const now = (/* @__PURE__ */ new Date()).toISOString();
85
- return {
86
- ...doc,
87
- _id: doc._id,
88
- _type: doc._type,
89
- _rev: doc._rev ?? cryptoRandomKey(),
90
- _createdAt: doc._createdAt ?? now,
91
- _updatedAt: doc._updatedAt ?? now
92
- };
93
- }, seed = (documents) => {
94
- docs.clear();
95
- for (const d of documents) {
96
- const promoted = promote2(d);
97
- docs.set(promoted._id, promoted);
98
- }
99
- };
100
- return initial && seed(initial), {
101
- seed,
102
- add: (documents) => {
103
- for (const d of documents) {
104
- const promoted = promote2(d);
105
- docs.set(promoted._id, promoted);
106
- }
107
- },
108
- reset: () => docs.clear(),
109
- all: () => [...docs.values()],
110
- get: (id) => docs.get(id),
111
- put: (doc) => {
112
- docs.set(doc._id, doc);
113
- },
114
- remove: (id) => {
115
- const doc = docs.get(id);
116
- return doc && docs.delete(id), doc;
117
- },
118
- snapshot: () => [...docs.values()]
119
- };
120
- }
121
- function cryptoRandomKey(length = 12) {
122
- const bytes = new Uint8Array(length);
123
- return globalThis.crypto.getRandomValues(bytes), [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, length);
124
- }
125
121
  function describeType(value) {
126
122
  return value === null ? "null" : Array.isArray(value) ? "an array" : `a ${typeof value}`;
127
123
  }
@@ -324,13 +320,140 @@ function insertIntoArray(doc, position, anchor, items) {
324
320
  const idx = resolveSelector(arr, last.selector);
325
321
  position === "replace" ? arr.splice(idx, 1, ...items) : position === "before" ? arr.splice(idx, 0, ...items) : arr.splice(idx + 1, 0, ...items);
326
322
  }
327
- const RELEASE_DOC_PREFIX$1 = "_.releases.", VERSIONS_PREFIX$1 = "versions.", releaseDocId = (releaseId) => `${RELEASE_DOC_PREFIX$1}${releaseId}`, versionDocId = (releaseId, publishedId) => `${VERSIONS_PREFIX$1}${releaseId}.${publishedId}`;
323
+ const DRAFTS_PREFIX = "drafts.", VERSIONS_PREFIX$1 = "versions.", RELEASE_DOC_PREFIX$1 = "_.releases.";
324
+ function publishedIdOf(id) {
325
+ if (id.startsWith(DRAFTS_PREFIX)) return id.slice(DRAFTS_PREFIX.length);
326
+ if (id.startsWith(VERSIONS_PREFIX$1)) {
327
+ const rest = id.slice(VERSIONS_PREFIX$1.length), dot = rest.indexOf(".");
328
+ return dot === -1 ? rest : rest.slice(dot + 1);
329
+ }
330
+ return id;
331
+ }
332
+ function releaseNameOf(id) {
333
+ if (!id.startsWith(VERSIONS_PREFIX$1)) return;
334
+ const rest = id.slice(VERSIONS_PREFIX$1.length), dot = rest.indexOf(".");
335
+ if (dot !== -1)
336
+ return rest.slice(0, dot);
337
+ }
338
+ const isDraftId = (id) => id.startsWith(DRAFTS_PREFIX), isVersionId = (id) => id.startsWith(VERSIONS_PREFIX$1), isSystemId = (id) => id.startsWith("_."), isReleaseDocId = (id) => id.startsWith(RELEASE_DOC_PREFIX$1);
339
+ function applyPerspective(docs, perspective) {
340
+ if (perspective === "raw") return [...docs];
341
+ if (perspective === "published")
342
+ return docs.filter((d) => !isDraftId(d._id) && !isVersionId(d._id) && !isSystemId(d._id));
343
+ if (perspective === "drafts")
344
+ return mergeStack(docs, [], { includeDrafts: !0, includeReleaseDocs: !1 });
345
+ const stack = [];
346
+ let includeDrafts = !1;
347
+ for (const entry of perspective)
348
+ entry === "drafts" ? includeDrafts = !0 : stack.push(entry);
349
+ return mergeStack(docs, stack, { includeDrafts, includeReleaseDocs: !0 });
350
+ }
351
+ function mergeStack(docs, releaseStack, options) {
352
+ const groups = /* @__PURE__ */ new Map(), systemReleases = [];
353
+ for (const doc of docs) {
354
+ if (isReleaseDocId(doc._id)) {
355
+ options.includeReleaseDocs && systemReleases.push(doc);
356
+ continue;
357
+ }
358
+ if (isSystemId(doc._id)) continue;
359
+ const pubId = publishedIdOf(doc._id);
360
+ let group = groups.get(pubId);
361
+ if (group === void 0 && (group = { versions: /* @__PURE__ */ new Map() }, groups.set(pubId, group)), isDraftId(doc._id))
362
+ group.draft = doc;
363
+ else if (isVersionId(doc._id)) {
364
+ const releaseName = releaseNameOf(doc._id);
365
+ releaseName !== void 0 && group.versions.set(releaseName, doc);
366
+ } else
367
+ group.published = doc;
368
+ }
369
+ const out = [];
370
+ for (const [pubId, group] of groups) {
371
+ let chosen;
372
+ for (const releaseName of releaseStack) {
373
+ const v = group.versions.get(releaseName);
374
+ if (v !== void 0) {
375
+ if (isUnpublishStub(v)) {
376
+ chosen = void 0;
377
+ break;
378
+ }
379
+ chosen = v;
380
+ break;
381
+ }
382
+ }
383
+ chosen === void 0 && options.includeDrafts && group.draft !== void 0 && (chosen = group.draft), chosen === void 0 && group.published !== void 0 && (releaseStack.some((name) => {
384
+ const v = group.versions.get(name);
385
+ return v !== void 0 && isUnpublishStub(v);
386
+ }) || (chosen = group.published)), chosen !== void 0 && out.push(projectToPublishedId(chosen, pubId));
387
+ }
388
+ return [...out, ...systemReleases];
389
+ }
390
+ function isUnpublishStub(doc) {
391
+ return doc._system?.delete === !0;
392
+ }
393
+ function projectToPublishedId(doc, pubId) {
394
+ return doc._id === pubId ? doc : { ...doc, _id: pubId, _originalId: doc._id };
395
+ }
396
+ function resolveDocument(docs, id, perspective) {
397
+ return perspective === "raw" ? docs.find((d) => d._id === id) : applyPerspective(docs, perspective).find((d) => d._id === id);
398
+ }
399
+ function createTestStore(initial) {
400
+ const docs = /* @__PURE__ */ new Map(), promote2 = (doc) => {
401
+ if (typeof doc._id != "string" || doc._id.length === 0)
402
+ throw fakeError(
403
+ `Cannot seed/store a document without a non-empty string \`_id\` (got ${JSON.stringify(doc._id)}). This fake never auto-generates ids \u2014 give every document an explicit _id. Document: ${JSON.stringify(doc)}`
404
+ );
405
+ if (typeof doc._type != "string" || doc._type.length === 0)
406
+ throw fakeError(
407
+ `Document "${doc._id}" is missing a non-empty string \`_type\` (got ${JSON.stringify(doc._type)}). Every Sanity document needs a _type.`
408
+ );
409
+ const now = (/* @__PURE__ */ new Date()).toISOString();
410
+ return {
411
+ ...doc,
412
+ _id: doc._id,
413
+ _type: doc._type,
414
+ _rev: doc._rev ?? cryptoRandomKey(),
415
+ _createdAt: doc._createdAt ?? now,
416
+ _updatedAt: doc._updatedAt ?? now
417
+ };
418
+ }, seed = (documents) => {
419
+ docs.clear();
420
+ for (const d of documents) {
421
+ const promoted = promote2(d);
422
+ docs.set(promoted._id, promoted);
423
+ }
424
+ };
425
+ return initial && seed(initial), {
426
+ seed,
427
+ add: (documents) => {
428
+ for (const d of documents) {
429
+ const promoted = promote2(d);
430
+ docs.set(promoted._id, promoted);
431
+ }
432
+ },
433
+ reset: () => docs.clear(),
434
+ all: () => [...docs.values()],
435
+ get: (id) => docs.get(id),
436
+ put: (doc) => {
437
+ docs.set(doc._id, doc);
438
+ },
439
+ remove: (id) => {
440
+ const doc = docs.get(id);
441
+ return doc && docs.delete(id), doc;
442
+ },
443
+ snapshot: () => [...docs.values()]
444
+ };
445
+ }
446
+ function cryptoRandomKey(length = 12) {
447
+ const bytes = new Uint8Array(length);
448
+ return globalThis.crypto.getRandomValues(bytes), [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, length);
449
+ }
450
+ const RELEASE_DOC_PREFIX = "_.releases.", VERSIONS_PREFIX = "versions.", releaseDocId = (releaseId) => `${RELEASE_DOC_PREFIX}${releaseId}`, versionDocId = (releaseId, publishedId) => `${VERSIONS_PREFIX}${releaseId}.${publishedId}`;
328
451
  function parseVersionId(versionId) {
329
- if (!versionId.startsWith(VERSIONS_PREFIX$1))
452
+ if (!versionId.startsWith(VERSIONS_PREFIX))
330
453
  throw fakeError(
331
454
  `"${versionId}" is not a version document id. Version ids look like "versions.<releaseId>.<publishedId>" (e.g. "versions.summer-drop.product-1").`
332
455
  );
333
- const rest = versionId.slice(VERSIONS_PREFIX$1.length), dot = rest.indexOf(".");
456
+ const rest = versionId.slice(VERSIONS_PREFIX.length), dot = rest.indexOf(".");
334
457
  if (dot === -1)
335
458
  throw fakeError(
336
459
  `"${versionId}" is not a version document id. Version ids look like "versions.<releaseId>.<publishedId>" (e.g. "versions.summer-drop.product-1").`
@@ -376,7 +499,7 @@ function requireActive(release) {
376
499
  `Release "${release.name}" is "${release.state}", but only "active" releases accept version writes (create / replace / discard / unpublish). Unschedule or unarchive it back to "active" first, or target a different release.`
377
500
  );
378
501
  }
379
- function applyAction(store, action) {
502
+ async function applyAction(store, action, hooks) {
380
503
  switch (action.actionType) {
381
504
  case "sanity.action.release.create": {
382
505
  if (maybeRelease(store, action.releaseId) !== void 0)
@@ -434,7 +557,7 @@ function applyAction(store, action) {
434
557
  throw fakeError(
435
558
  `Cannot publish release "${action.releaseId}": it is "${release.state}", and only "active" or "scheduled" releases can be published.`
436
559
  );
437
- const prefix = `${VERSIONS_PREFIX$1}${action.releaseId}.`, sources = store.all().filter((d) => d._id.startsWith(prefix));
560
+ const prefix = `${VERSIONS_PREFIX}${action.releaseId}.`, sources = store.all().filter((d) => d._id.startsWith(prefix));
438
561
  for (const v of sources) {
439
562
  const pubId = v._id.slice(prefix.length);
440
563
  if (v._system?.delete === !0)
@@ -474,7 +597,7 @@ function applyAction(store, action) {
474
597
  throw fakeError(
475
598
  `Cannot delete release "${action.releaseId}": it is "${release.state}", and only "archived" or "published" releases can be deleted. Archive it first (sanity.action.release.archive) or publish it.`
476
599
  );
477
- const prefix = `${VERSIONS_PREFIX$1}${action.releaseId}.`;
600
+ const prefix = `${VERSIONS_PREFIX}${action.releaseId}.`;
478
601
  for (const v of store.all())
479
602
  v._id.startsWith(prefix) && store.remove(v._id);
480
603
  return store.remove(release._id), { transactionId: cryptoRandomKey() };
@@ -518,9 +641,46 @@ function applyAction(store, action) {
518
641
  };
519
642
  return store.put(promote(doc)), { transactionId: cryptoRandomKey() };
520
643
  }
644
+ case "sanity.action.document.publish": {
645
+ requireDraftOf(action);
646
+ const draft = store.get(action.draftId);
647
+ if (draft === void 0)
648
+ throw fakeError(
649
+ `Cannot publish "${action.publishedId}": draft "${action.draftId}" not found. document.publish promotes an existing draft onto the published id \u2014 create the draft first (e.g. client.createOrReplace({ _id: "${action.draftId}", ... })).`
650
+ );
651
+ const published = store.get(action.publishedId);
652
+ requireRevisionMatch(action.draftId, draft._rev, action.ifDraftRevisionId), requireRevisionMatch(action.publishedId, published?._rev, action.ifPublishedRevisionId);
653
+ const promoted = promote({ ...draft, _id: action.publishedId });
654
+ 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() };
655
+ }
656
+ case "sanity.action.document.unpublish": {
657
+ requireDraftOf(action);
658
+ const published = store.get(action.publishedId);
659
+ if (published === void 0)
660
+ throw fakeError(
661
+ `Cannot unpublish "${action.publishedId}": no published document with that _id exists. document.unpublish retracts a live published document into "${action.draftId}".`
662
+ );
663
+ if (await hooks?.gate?.(published, "update"), await hooks?.guard?.("unpublish", published, null), store.get(action.draftId) === void 0) {
664
+ const draft = promote({ ...published, _id: action.draftId });
665
+ draft._createdAt = published._createdAt, store.put(draft);
666
+ }
667
+ return store.remove(action.publishedId), { transactionId: cryptoRandomKey() };
668
+ }
521
669
  }
522
670
  }
523
- function applyActions(store, actions) {
671
+ function requireDraftOf(action) {
672
+ if (!(publishedIdOf(action.draftId) === action.publishedId && action.draftId !== action.publishedId))
673
+ throw fakeError(
674
+ `${action.actionType}: draftId "${action.draftId}" is not the draft of publishedId "${action.publishedId}" \u2014 expected draftId "drafts.${action.publishedId}".`
675
+ );
676
+ }
677
+ function requireRevisionMatch(documentId, actualRev, expectedRev) {
678
+ if (!(expectedRev === void 0 || actualRev === expectedRev))
679
+ throw fakeError(
680
+ `Revision precondition failed for "${documentId}": expected _rev "${expectedRev}" but the document is at ${actualRev === void 0 ? "no revision (missing)" : `"${actualRev}"`}.`
681
+ );
682
+ }
683
+ async function applyActions(store, actions, hooks) {
524
684
  const list = Array.isArray(actions) ? actions : [actions], snapshot = /* @__PURE__ */ new Map(), remember = (id) => {
525
685
  snapshot.has(id) || snapshot.set(id, store.get(id));
526
686
  };
@@ -535,11 +695,11 @@ function applyActions(store, actions) {
535
695
  case "sanity.action.release.archive":
536
696
  case "sanity.action.release.unarchive":
537
697
  case "sanity.action.release.delete":
538
- remember(`${RELEASE_DOC_PREFIX$1}${a.releaseId}`);
698
+ remember(`${RELEASE_DOC_PREFIX}${a.releaseId}`);
539
699
  break;
540
700
  case "sanity.action.release.publish": {
541
- remember(`${RELEASE_DOC_PREFIX$1}${a.releaseId}`);
542
- const prefix = `${VERSIONS_PREFIX$1}${a.releaseId}.`;
701
+ remember(`${RELEASE_DOC_PREFIX}${a.releaseId}`);
702
+ const prefix = `${VERSIONS_PREFIX}${a.releaseId}.`;
543
703
  for (const d of store.all())
544
704
  d._id.startsWith(prefix) && (remember(d._id), remember(d._id.slice(prefix.length)));
545
705
  break;
@@ -556,8 +716,12 @@ function applyActions(store, actions) {
556
716
  case "sanity.action.document.version.unpublish":
557
717
  remember(a.versionId);
558
718
  break;
719
+ case "sanity.action.document.publish":
720
+ case "sanity.action.document.unpublish":
721
+ remember(a.draftId), remember(a.publishedId);
722
+ break;
559
723
  }
560
- last = applyAction(store, a);
724
+ last = await applyAction(store, a, hooks);
561
725
  }
562
726
  return last;
563
727
  } catch (err) {
@@ -566,82 +730,6 @@ function applyActions(store, actions) {
566
730
  throw err;
567
731
  }
568
732
  }
569
- const DRAFTS_PREFIX = "drafts.", VERSIONS_PREFIX = "versions.", RELEASE_DOC_PREFIX = "_.releases.";
570
- function publishedIdOf(id) {
571
- if (id.startsWith(DRAFTS_PREFIX)) return id.slice(DRAFTS_PREFIX.length);
572
- if (id.startsWith(VERSIONS_PREFIX)) {
573
- const rest = id.slice(VERSIONS_PREFIX.length), dot = rest.indexOf(".");
574
- return dot === -1 ? rest : rest.slice(dot + 1);
575
- }
576
- return id;
577
- }
578
- function releaseNameOf(id) {
579
- if (!id.startsWith(VERSIONS_PREFIX)) return;
580
- const rest = id.slice(VERSIONS_PREFIX.length), dot = rest.indexOf(".");
581
- if (dot !== -1)
582
- return rest.slice(0, dot);
583
- }
584
- const isDraftId = (id) => id.startsWith(DRAFTS_PREFIX), isVersionId = (id) => id.startsWith(VERSIONS_PREFIX), isSystemId = (id) => id.startsWith("_."), isReleaseDocId = (id) => id.startsWith(RELEASE_DOC_PREFIX);
585
- function applyPerspective(docs, perspective) {
586
- if (perspective === "raw") return [...docs];
587
- if (perspective === "published")
588
- return docs.filter((d) => !isDraftId(d._id) && !isVersionId(d._id) && !isSystemId(d._id));
589
- if (perspective === "drafts")
590
- return mergeStack(docs, [], { includeDrafts: !0, includeReleaseDocs: !1 });
591
- const stack = [];
592
- let includeDrafts = !1;
593
- for (const entry of perspective)
594
- entry === "drafts" ? includeDrafts = !0 : stack.push(entry);
595
- return mergeStack(docs, stack, { includeDrafts, includeReleaseDocs: !0 });
596
- }
597
- function mergeStack(docs, releaseStack, options) {
598
- const groups = /* @__PURE__ */ new Map(), systemReleases = [];
599
- for (const doc of docs) {
600
- if (isReleaseDocId(doc._id)) {
601
- options.includeReleaseDocs && systemReleases.push(doc);
602
- continue;
603
- }
604
- if (isSystemId(doc._id)) continue;
605
- const pubId = publishedIdOf(doc._id);
606
- let group = groups.get(pubId);
607
- if (group === void 0 && (group = { versions: /* @__PURE__ */ new Map() }, groups.set(pubId, group)), isDraftId(doc._id))
608
- group.draft = doc;
609
- else if (isVersionId(doc._id)) {
610
- const releaseName = releaseNameOf(doc._id);
611
- releaseName !== void 0 && group.versions.set(releaseName, doc);
612
- } else
613
- group.published = doc;
614
- }
615
- const out = [];
616
- for (const [pubId, group] of groups) {
617
- let chosen;
618
- for (const releaseName of releaseStack) {
619
- const v = group.versions.get(releaseName);
620
- if (v !== void 0) {
621
- if (isUnpublishStub(v)) {
622
- chosen = void 0;
623
- break;
624
- }
625
- chosen = v;
626
- break;
627
- }
628
- }
629
- chosen === void 0 && options.includeDrafts && group.draft !== void 0 && (chosen = group.draft), chosen === void 0 && group.published !== void 0 && (releaseStack.some((name) => {
630
- const v = group.versions.get(name);
631
- return v !== void 0 && isUnpublishStub(v);
632
- }) || (chosen = group.published)), chosen !== void 0 && out.push(projectToPublishedId(chosen, pubId));
633
- }
634
- return [...out, ...systemReleases];
635
- }
636
- function isUnpublishStub(doc) {
637
- return doc._system?.delete === !0;
638
- }
639
- function projectToPublishedId(doc, pubId) {
640
- return doc._id === pubId ? doc : { ...doc, _id: pubId, _originalId: doc._id };
641
- }
642
- function resolveDocument(docs, id, perspective) {
643
- return perspective === "raw" ? docs.find((d) => d._id === id) : applyPerspective(docs, perspective).find((d) => d._id === id);
644
- }
645
733
  function createTransaction(executor) {
646
734
  const mutations = [], handle = {
647
735
  create: (doc) => (mutations.push({ kind: "create", doc }), handle),
@@ -737,7 +825,7 @@ const ISSUES_URL = "https://github.com/sanity-labs/client-fake-for-test/issues",
737
825
  },
738
826
  request: {
739
827
  what: "generic HTTP request hook \u2014 only wired up when the handle has an acting identity",
740
- workaround: "Create the client with `accessControl`, or bind a registered token (`identities` + `token` / `withConfig({ token })`), to enable request() (it serves /users/me + the ACL path). Or register a stub."
828
+ workaround: "Create the client with `accessControl`, or bind a registered token (`identities` + `token` / `withConfig({ token })`), to enable request() (it serves /users/me, the ACL path, and /projects/<projectId>/users/<ids>). Or register a stub."
741
829
  },
742
830
  dryRun: {
743
831
  what: "not a method \u2014 did you mean the `{ dryRun }` mutation option?",
@@ -920,15 +1008,16 @@ function createTestClient(options = {}) {
920
1008
  const { projectId, dataset } = parseRegistryKey(key);
921
1009
  ensureStore(projectId, dataset).seed(docs);
922
1010
  }
923
- return makeHandle(
1011
+ return makeHandle({
924
1012
  backend,
925
- defaultProjectId,
926
- defaultDataset,
927
- options.accessControl,
928
- options.perspective ?? "raw",
929
- options.mutationGuard,
930
- options.token
931
- );
1013
+ projectId: defaultProjectId,
1014
+ dataset: defaultDataset,
1015
+ accessControl: options.accessControl,
1016
+ perspective: options.perspective ?? "raw",
1017
+ mutationGuard: options.mutationGuard,
1018
+ token: options.token,
1019
+ useProjectHostname: options.useProjectHostname ?? !0
1020
+ });
932
1021
  }
933
1022
  function dedupeIds(results) {
934
1023
  const seen = /* @__PURE__ */ new Set(), out = [];
@@ -980,13 +1069,26 @@ function toTxnMutations(operations) {
980
1069
  }
981
1070
  ];
982
1071
  }
983
- function makeHandle(backend, projectId, dataset, accessControl, perspective, mutationGuard, token) {
984
- const { registry } = backend, activeAccess = () => {
1072
+ function makeHandle(handleArgs) {
1073
+ const {
1074
+ backend,
1075
+ projectId,
1076
+ dataset,
1077
+ accessControl,
1078
+ perspective,
1079
+ mutationGuard,
1080
+ token,
1081
+ useProjectHostname
1082
+ } = handleArgs, { registry } = backend, activeAccess = () => {
985
1083
  if (token === void 0) return accessControl;
986
1084
  const identity = backend.identities.get(token);
987
1085
  if (identity === void 0)
988
1086
  throw fakeError(`no identity is registered for token "${token}" (was it removed?).`);
989
- return { currentUser: identity.user, grants: identity.grants };
1087
+ return {
1088
+ currentUser: projectUserOf(identity, projectId),
1089
+ ...identity.globalUser !== void 0 ? { globalUser: identity.globalUser } : {},
1090
+ grants: identity.grants
1091
+ };
990
1092
  }, requireActiveAccess = () => {
991
1093
  const access = activeAccess();
992
1094
  if (access === void 0)
@@ -1136,7 +1238,7 @@ function makeHandle(backend, projectId, dataset, accessControl, perspective, mut
1136
1238
  txnOptions?.returnDocuments ?? !1,
1137
1239
  txnOptions?.returnFirst ?? !1
1138
1240
  );
1139
- }, runAction = (actions) => serializeWrite(backend, async () => applyActions(store, actions)), accessVisibleDocs = async () => {
1241
+ }, runAction = (actions) => serializeWrite(backend, async () => applyActions(store, actions, { gate, guard: guardGate })), accessVisibleDocs = async () => {
1140
1242
  const all = store.all();
1141
1243
  if (activeAccess() === void 0) return all;
1142
1244
  const out = [];
@@ -1307,20 +1409,35 @@ function makeHandle(backend, projectId, dataset, accessControl, perspective, mut
1307
1409
  versionId: `versions.${releaseId}.${publishedId}`,
1308
1410
  publishedId
1309
1411
  }),
1310
- ...token !== void 0 || accessControl !== void 0 ? { request: makeRequestImpl(requireActiveAccess, projectId, dataset) } : {},
1311
- config: () => ({ projectId, dataset, perspective, ...token !== void 0 ? { token } : {} }),
1412
+ ...token !== void 0 || accessControl !== void 0 ? {
1413
+ request: makeRequestImpl({
1414
+ getAccess: requireActiveAccess,
1415
+ projectId,
1416
+ dataset,
1417
+ useProjectHostname,
1418
+ listIdentities: () => [...backend.identities.values()]
1419
+ })
1420
+ } : {},
1421
+ config: () => ({
1422
+ projectId,
1423
+ dataset,
1424
+ perspective,
1425
+ ...token !== void 0 ? { token } : {},
1426
+ useProjectHostname
1427
+ }),
1312
1428
  withConfig: (config) => {
1313
1429
  rejectAmbiguousIdentity("withConfig()", config), config.token !== void 0 && requireIdentity(backend, config.token, "withConfig()");
1314
1430
  const rebinds = config.token !== void 0 || config.accessControl !== void 0;
1315
- return makeHandle(
1431
+ return makeHandle({
1316
1432
  backend,
1317
- config.projectId ?? projectId,
1318
- config.dataset ?? dataset,
1319
- rebinds ? config.accessControl : accessControl,
1320
- config.perspective ?? perspective,
1433
+ projectId: config.projectId ?? projectId,
1434
+ dataset: config.dataset ?? dataset,
1435
+ accessControl: rebinds ? config.accessControl : accessControl,
1436
+ perspective: config.perspective ?? perspective,
1321
1437
  mutationGuard,
1322
- rebinds ? config.token : token
1323
- );
1438
+ token: rebinds ? config.token : token,
1439
+ useProjectHostname: config.useProjectHostname ?? useProjectHostname
1440
+ });
1324
1441
  },
1325
1442
  seed: (documents) => store.seed(documents),
1326
1443
  add: (documents) => store.add(documents),