@sanity-labs/client-fake-for-test 0.1.0 → 0.3.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.cjs CHANGED
@@ -1,6 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: !0 });
3
3
  var groqJs = require("groq-js");
4
+ const ERR_PREFIX = "[client-fake-for-test]";
5
+ function fakeError(message) {
6
+ return new Error(`${ERR_PREFIX} ${message}`);
7
+ }
4
8
  class WriteAccessDeniedError extends Error {
5
9
  documentId;
6
10
  permission;
@@ -65,11 +69,65 @@ function makeRequestImpl(accessControl, projectId, dataset) {
65
69
  return accessControl.currentUser;
66
70
  if (path === aclPath)
67
71
  return accessControl.grants;
68
- throw new Error(
69
- `TestClient.request: no handler for path "${path}". The TestClient only serves "/users/me" and "${aclPath}". Configure additional endpoints via createTestClient({ accessControl }).`
72
+ throw fakeError(
73
+ `request(): no handler for path "${path}". With accessControl 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.`
70
74
  );
71
75
  };
72
76
  }
77
+ function createTestStore(initial) {
78
+ const docs = /* @__PURE__ */ new Map(), promote2 = (doc) => {
79
+ if (typeof doc._id != "string" || doc._id.length === 0)
80
+ throw fakeError(
81
+ `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)}`
82
+ );
83
+ if (typeof doc._type != "string" || doc._type.length === 0)
84
+ throw fakeError(
85
+ `Document "${doc._id}" is missing a non-empty string \`_type\` (got ${JSON.stringify(doc._type)}). Every Sanity document needs a _type.`
86
+ );
87
+ const now = (/* @__PURE__ */ new Date()).toISOString();
88
+ return {
89
+ ...doc,
90
+ _id: doc._id,
91
+ _type: doc._type,
92
+ _rev: doc._rev ?? cryptoRandomKey(),
93
+ _createdAt: doc._createdAt ?? now,
94
+ _updatedAt: doc._updatedAt ?? now
95
+ };
96
+ }, seed = (documents) => {
97
+ docs.clear();
98
+ for (const d of documents) {
99
+ const promoted = promote2(d);
100
+ docs.set(promoted._id, promoted);
101
+ }
102
+ };
103
+ return initial && seed(initial), {
104
+ seed,
105
+ add: (documents) => {
106
+ for (const d of documents) {
107
+ const promoted = promote2(d);
108
+ docs.set(promoted._id, promoted);
109
+ }
110
+ },
111
+ reset: () => docs.clear(),
112
+ all: () => [...docs.values()],
113
+ get: (id) => docs.get(id),
114
+ put: (doc) => {
115
+ docs.set(doc._id, doc);
116
+ },
117
+ remove: (id) => {
118
+ const doc = docs.get(id);
119
+ return doc && docs.delete(id), doc;
120
+ },
121
+ snapshot: () => [...docs.values()]
122
+ };
123
+ }
124
+ function cryptoRandomKey(length = 12) {
125
+ const bytes = new Uint8Array(length);
126
+ return globalThis.crypto.getRandomValues(bytes), [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, length);
127
+ }
128
+ function describeType(value) {
129
+ return value === null ? "null" : Array.isArray(value) ? "an array" : `a ${typeof value}`;
130
+ }
73
131
  function createPatch(documentId, executor) {
74
132
  const ops = [];
75
133
  let ifRev;
@@ -130,7 +188,10 @@ function parsePath(path) {
130
188
  const segments = [];
131
189
  for (const raw of path.split(".")) {
132
190
  const match = /^([A-Za-z_$][\w$]*)(?:\[(.+)\])?$/.exec(raw);
133
- if (!match) throw new Error(`Unsupported patch path segment: ${raw}`);
191
+ if (!match)
192
+ throw fakeError(
193
+ `Unsupported patch path segment "${raw}" (in path "${path}"). This fake supports dotted field access ("a.b.c"), array index ("items[0]"), and keyed lookup (\`items[_key=="x"]\`). Recursive descent ("..") and GROQ filter expressions are not supported.`
194
+ );
134
195
  const key = match[1], selectorRaw = match[2];
135
196
  if (selectorRaw === void 0) {
136
197
  segments.push({ key });
@@ -149,7 +210,9 @@ function parsePath(path) {
149
210
  });
150
211
  continue;
151
212
  }
152
- throw new Error(`Unsupported array selector in patch path: ${selectorRaw}`);
213
+ throw fakeError(
214
+ `Unsupported array selector "[${selectorRaw}]" in patch path "${path}". Supported selectors are a numeric index (e.g. "[0]") and a single keyed match (e.g. \`[_key=="abc"]\`). Multi-key matches and arbitrary GROQ filters are not supported.`
215
+ );
153
216
  }
154
217
  return segments;
155
218
  }
@@ -159,7 +222,9 @@ function resolveSelector(array, selector) {
159
222
  (entry) => typeof entry == "object" && entry !== null && entry[selector.field] === selector.value
160
223
  );
161
224
  if (idx === -1)
162
- throw new Error(`No array entry matched ${selector.field}=="${selector.value}" in patch path`);
225
+ throw fakeError(
226
+ `Patch path matched no array entry where ${selector.field}=="${selector.value}". The array has ${array.length} item(s); check the key exists (and that you're patching the right document).`
227
+ );
163
228
  return idx;
164
229
  }
165
230
  function getAtPath(doc, path) {
@@ -177,14 +242,17 @@ function getAtPath(doc, path) {
177
242
  }
178
243
  function setAtPath(doc, path, value) {
179
244
  const segments = parsePath(path);
180
- if (segments.length === 0) throw new Error("Empty patch path");
245
+ if (segments.length === 0)
246
+ throw fakeError("Empty patch path \u2014 a set/setIfMissing op needs a target field.");
181
247
  let cursor = doc;
182
248
  for (let i = 0; i < segments.length - 1; i++) {
183
249
  const seg = segments[i];
184
250
  let next = cursor[seg.key];
185
251
  if (next === void 0 && (next = seg.selector?.kind === "index" || seg.selector?.kind === "keyed" ? [] : {}, cursor[seg.key] = next), seg.selector !== void 0) {
186
252
  if (!Array.isArray(next))
187
- throw new Error(`Patch path crosses non-array at segment ${seg.key}`);
253
+ throw fakeError(
254
+ `Patch path "${path}" applies an array selector to field "${seg.key}", but that field holds ${describeType(next)}, not an array.`
255
+ );
188
256
  const idx = resolveSelector(next, seg.selector);
189
257
  cursor = next[idx];
190
258
  } else
@@ -194,7 +262,9 @@ function setAtPath(doc, path, value) {
194
262
  if (last.selector !== void 0) {
195
263
  const arr = cursor[last.key];
196
264
  if (!Array.isArray(arr))
197
- throw new Error(`Patch path crosses non-array at segment ${last.key}`);
265
+ throw fakeError(
266
+ `Patch path "${path}" applies an array selector to field "${last.key}", but that field holds ${describeType(arr)}, not an array.`
267
+ );
198
268
  const idx = resolveSelector(arr, last.selector);
199
269
  arr[idx] = value;
200
270
  } else
@@ -226,74 +296,354 @@ function unsetAtPath(doc, path) {
226
296
  }
227
297
  function insertIntoArray(doc, position, anchor, items) {
228
298
  const segments = parsePath(anchor);
229
- if (segments.length === 0) throw new Error("Empty insert anchor");
299
+ if (segments.length === 0)
300
+ throw fakeError("Empty insert anchor \u2014 insert() needs an array path to anchor against.");
230
301
  const last = segments[segments.length - 1];
231
302
  if (last.selector === void 0)
232
- throw new Error(`Insert anchor must end with an array selector: ${anchor}`);
303
+ throw fakeError(
304
+ `Insert anchor "${anchor}" must end with an array selector (an index like "[0]" or a keyed match like \`[_key=="x"]\`) so the fake knows where in the array to insert.`
305
+ );
233
306
  let cursor = doc;
234
307
  for (let i = 0; i < segments.length - 1; i++) {
235
308
  const seg = segments[i], next = cursor[seg.key];
236
309
  if (next == null)
237
- throw new Error(`Insert anchor traverses missing field: ${seg.key}`);
310
+ throw fakeError(
311
+ `Insert anchor "${anchor}" traverses field "${seg.key}", which is missing on the document. setIfMissing the parent first, or anchor against an existing array.`
312
+ );
238
313
  if (seg.selector !== void 0) {
239
314
  if (!Array.isArray(next))
240
- throw new Error(`Insert anchor traverses non-array at: ${seg.key}`);
315
+ throw fakeError(
316
+ `Insert anchor "${anchor}" applies an array selector to field "${seg.key}", but it holds ${describeType(next)}, not an array.`
317
+ );
241
318
  cursor = next[resolveSelector(next, seg.selector)];
242
319
  } else
243
320
  cursor = next;
244
321
  }
245
322
  const arr = cursor[last.key];
246
323
  if (!Array.isArray(arr))
247
- throw new Error(`Insert anchor target is not an array: ${last.key}`);
324
+ throw fakeError(
325
+ `Insert anchor "${anchor}" targets field "${last.key}", but it holds ${describeType(arr)}, not an array.`
326
+ );
248
327
  const idx = resolveSelector(arr, last.selector);
249
328
  position === "replace" ? arr.splice(idx, 1, ...items) : position === "before" ? arr.splice(idx, 0, ...items) : arr.splice(idx + 1, 0, ...items);
250
329
  }
251
- function createTestStore(initial) {
252
- const docs = /* @__PURE__ */ new Map(), promote = (doc) => {
253
- if (typeof doc._id != "string" || doc._id.length === 0)
254
- throw new Error(`document is missing _id: ${JSON.stringify(doc)}`);
255
- if (typeof doc._type != "string" || doc._type.length === 0)
256
- throw new Error(`document ${doc._id} is missing _type`);
257
- const now = (/* @__PURE__ */ new Date()).toISOString();
258
- return {
259
- ...doc,
260
- _id: doc._id,
261
- _type: doc._type,
262
- _rev: doc._rev ?? cryptoRandomKey(),
263
- _createdAt: doc._createdAt ?? now,
264
- _updatedAt: doc._updatedAt ?? now
265
- };
266
- }, seed = (documents) => {
267
- docs.clear();
268
- for (const d of documents) {
269
- const promoted = promote(d);
270
- docs.set(promoted._id, promoted);
271
- }
330
+ 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}`;
331
+ function parseVersionId(versionId) {
332
+ if (!versionId.startsWith(VERSIONS_PREFIX$1))
333
+ throw fakeError(
334
+ `"${versionId}" is not a version document id. Version ids look like "versions.<releaseId>.<publishedId>" (e.g. "versions.summer-drop.product-1").`
335
+ );
336
+ const rest = versionId.slice(VERSIONS_PREFIX$1.length), dot = rest.indexOf(".");
337
+ if (dot === -1)
338
+ throw fakeError(
339
+ `"${versionId}" is not a version document id. Version ids look like "versions.<releaseId>.<publishedId>" (e.g. "versions.summer-drop.product-1").`
340
+ );
341
+ return { releaseId: rest.slice(0, dot), publishedId: rest.slice(dot + 1) };
342
+ }
343
+ function getRelease(store, releaseId) {
344
+ const doc = store.get(releaseDocId(releaseId));
345
+ if (doc === void 0)
346
+ throw fakeError(
347
+ `Release "${releaseId}" not found. Create it first with client.releases.create({ releaseId: "${releaseId}" }) (or a "sanity.action.release.create" action) before editing, scheduling, or adding versions to it.`
348
+ );
349
+ return doc;
350
+ }
351
+ function maybeRelease(store, releaseId) {
352
+ return store.get(releaseDocId(releaseId));
353
+ }
354
+ function nowIso() {
355
+ return (/* @__PURE__ */ new Date()).toISOString();
356
+ }
357
+ function promote(doc) {
358
+ const now = nowIso();
359
+ return {
360
+ ...doc,
361
+ _id: doc._id,
362
+ _type: doc._type,
363
+ _rev: cryptoRandomKey(),
364
+ _createdAt: doc._createdAt ?? now,
365
+ _updatedAt: now
272
366
  };
273
- return initial && seed(initial), {
274
- seed,
275
- add: (documents) => {
276
- for (const d of documents) {
277
- const promoted = promote(d);
278
- docs.set(promoted._id, promoted);
367
+ }
368
+ function bumpRev(doc, patch) {
369
+ return {
370
+ ...doc,
371
+ ...patch,
372
+ _rev: cryptoRandomKey(),
373
+ _updatedAt: nowIso()
374
+ };
375
+ }
376
+ function requireActive(release) {
377
+ if (release.state !== "active")
378
+ throw fakeError(
379
+ `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.`
380
+ );
381
+ }
382
+ function applyAction(store, action) {
383
+ switch (action.actionType) {
384
+ case "sanity.action.release.create": {
385
+ if (maybeRelease(store, action.releaseId) !== void 0)
386
+ throw fakeError(
387
+ `Release "${action.releaseId}" already exists. release.create expects a new releaseId; use release.edit to change its metadata, or pick a different releaseId.`
388
+ );
389
+ const now = nowIso(), metadata = {
390
+ releaseType: "undecided",
391
+ ...action.metadata
392
+ }, doc = {
393
+ _id: releaseDocId(action.releaseId),
394
+ _type: "system.release",
395
+ _rev: cryptoRandomKey(),
396
+ _createdAt: now,
397
+ _updatedAt: now,
398
+ name: action.releaseId,
399
+ state: "active",
400
+ metadata
401
+ };
402
+ return store.put(doc), { transactionId: cryptoRandomKey() };
403
+ }
404
+ case "sanity.action.release.edit": {
405
+ const release = getRelease(store, action.releaseId), patchOps = [];
406
+ action.patch.set !== void 0 && patchOps.push({ kind: "set", props: action.patch.set }), action.patch.unset !== void 0 && patchOps.push({ kind: "unset", paths: action.patch.unset });
407
+ const patched = applyPatchOps(release, patchOps);
408
+ return store.put(bumpRev(patched, {})), { transactionId: cryptoRandomKey() };
409
+ }
410
+ case "sanity.action.release.schedule": {
411
+ const release = getRelease(store, action.releaseId);
412
+ if (release.state !== "active")
413
+ throw fakeError(
414
+ `Cannot schedule release "${action.releaseId}": it is "${release.state}", and only "active" releases can be scheduled.`
415
+ );
416
+ return store.put(
417
+ bumpRev(release, {
418
+ state: "scheduled",
419
+ publishAt: action.publishAt
420
+ })
421
+ ), { transactionId: cryptoRandomKey() };
422
+ }
423
+ case "sanity.action.release.unschedule": {
424
+ const release = getRelease(store, action.releaseId);
425
+ if (release.state !== "scheduled")
426
+ throw fakeError(
427
+ `Cannot unschedule release "${action.releaseId}": it is "${release.state}", and only "scheduled" releases can be unscheduled.`
428
+ );
429
+ const next = { ...release, state: "active" };
430
+ return delete next.publishAt, store.put(bumpRev(next, {})), { transactionId: cryptoRandomKey() };
431
+ }
432
+ case "sanity.action.release.publish": {
433
+ const release = getRelease(store, action.releaseId);
434
+ if (release.state === "published")
435
+ return { transactionId: cryptoRandomKey() };
436
+ if (release.state !== "active" && release.state !== "scheduled")
437
+ throw fakeError(
438
+ `Cannot publish release "${action.releaseId}": it is "${release.state}", and only "active" or "scheduled" releases can be published.`
439
+ );
440
+ const prefix = `${VERSIONS_PREFIX$1}${action.releaseId}.`, sources = store.all().filter((d) => d._id.startsWith(prefix));
441
+ for (const v of sources) {
442
+ const pubId = v._id.slice(prefix.length);
443
+ if (v._system?.delete === !0)
444
+ store.remove(pubId);
445
+ else {
446
+ const body = {
447
+ ...v,
448
+ _id: pubId
449
+ };
450
+ delete body._originalId, delete body._system;
451
+ const promoted = promote(body), existing = store.get(pubId);
452
+ existing !== void 0 && (promoted._createdAt = existing._createdAt), store.put(promoted);
453
+ }
454
+ store.remove(v._id);
279
455
  }
280
- },
281
- reset: () => docs.clear(),
282
- all: () => [...docs.values()],
283
- get: (id) => docs.get(id),
284
- put: (doc) => {
285
- docs.set(doc._id, doc);
286
- },
287
- remove: (id) => {
288
- const doc = docs.get(id);
289
- return doc && docs.delete(id), doc;
290
- },
291
- snapshot: () => [...docs.values()]
456
+ return store.put(bumpRev(release, { state: "published" })), { transactionId: cryptoRandomKey() };
457
+ }
458
+ case "sanity.action.release.archive": {
459
+ const release = getRelease(store, action.releaseId);
460
+ if (release.state !== "active" && release.state !== "scheduled")
461
+ throw fakeError(
462
+ `Cannot archive release "${action.releaseId}": it is "${release.state}", and only "active" or "scheduled" releases can be archived.`
463
+ );
464
+ return store.put(bumpRev(release, { state: "archived" })), { transactionId: cryptoRandomKey() };
465
+ }
466
+ case "sanity.action.release.unarchive": {
467
+ const release = getRelease(store, action.releaseId);
468
+ if (release.state !== "archived")
469
+ throw fakeError(
470
+ `Cannot unarchive release "${action.releaseId}": it is "${release.state}", and only "archived" releases can be unarchived.`
471
+ );
472
+ return store.put(bumpRev(release, { state: "active" })), { transactionId: cryptoRandomKey() };
473
+ }
474
+ case "sanity.action.release.delete": {
475
+ const release = getRelease(store, action.releaseId);
476
+ if (release.state !== "archived" && release.state !== "published")
477
+ throw fakeError(
478
+ `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.`
479
+ );
480
+ const prefix = `${VERSIONS_PREFIX$1}${action.releaseId}.`;
481
+ for (const v of store.all())
482
+ v._id.startsWith(prefix) && store.remove(v._id);
483
+ return store.remove(release._id), { transactionId: cryptoRandomKey() };
484
+ }
485
+ case "sanity.action.document.version.create": {
486
+ const releaseId = action.releaseId ?? (action.document._id !== void 0 ? parseVersionId(action.document._id).releaseId : void 0);
487
+ if (releaseId === void 0)
488
+ throw fakeError(
489
+ 'version.create needs a release to attach to: pass `releaseId`, or a `document._id` shaped "versions.<releaseId>.<publishedId>" so the release can be inferred.'
490
+ );
491
+ const release = getRelease(store, releaseId);
492
+ requireActive(release);
493
+ const vid = versionDocId(releaseId, action.publishedId), doc = {
494
+ ...action.document,
495
+ _id: vid
496
+ }, existing = store.get(vid), promoted = promote(doc);
497
+ return existing !== void 0 && (promoted._createdAt = existing._createdAt), store.put(promoted), { transactionId: cryptoRandomKey() };
498
+ }
499
+ case "sanity.action.document.version.replace": {
500
+ const id = action.document._id;
501
+ if (typeof id != "string")
502
+ throw fakeError(
503
+ 'version.replace needs `document._id` (a version id like "versions.<releaseId>.<publishedId>") to identify which version document to replace.'
504
+ );
505
+ const { releaseId } = parseVersionId(id), release = getRelease(store, releaseId);
506
+ requireActive(release);
507
+ const existing = store.get(id), promoted = promote(action.document);
508
+ return existing !== void 0 && (promoted._createdAt = existing._createdAt), store.put(promoted), { transactionId: cryptoRandomKey() };
509
+ }
510
+ case "sanity.action.document.version.discard": {
511
+ const { releaseId } = parseVersionId(action.versionId), release = getRelease(store, releaseId);
512
+ return requireActive(release), store.remove(action.versionId), { transactionId: cryptoRandomKey() };
513
+ }
514
+ case "sanity.action.document.version.unpublish": {
515
+ const { releaseId } = parseVersionId(action.versionId), release = getRelease(store, releaseId);
516
+ requireActive(release);
517
+ const doc = {
518
+ _id: action.versionId,
519
+ _type: "stub",
520
+ _system: { delete: !0 }
521
+ };
522
+ return store.put(promote(doc)), { transactionId: cryptoRandomKey() };
523
+ }
524
+ }
525
+ }
526
+ function applyActions(store, actions) {
527
+ const list = Array.isArray(actions) ? actions : [actions], snapshot = /* @__PURE__ */ new Map(), remember = (id) => {
528
+ snapshot.has(id) || snapshot.set(id, store.get(id));
292
529
  };
530
+ try {
531
+ let last = { transactionId: cryptoRandomKey() };
532
+ for (const a of list) {
533
+ switch (a.actionType) {
534
+ case "sanity.action.release.create":
535
+ case "sanity.action.release.edit":
536
+ case "sanity.action.release.schedule":
537
+ case "sanity.action.release.unschedule":
538
+ case "sanity.action.release.archive":
539
+ case "sanity.action.release.unarchive":
540
+ case "sanity.action.release.delete":
541
+ remember(`${RELEASE_DOC_PREFIX$1}${a.releaseId}`);
542
+ break;
543
+ case "sanity.action.release.publish": {
544
+ remember(`${RELEASE_DOC_PREFIX$1}${a.releaseId}`);
545
+ const prefix = `${VERSIONS_PREFIX$1}${a.releaseId}.`;
546
+ for (const d of store.all())
547
+ d._id.startsWith(prefix) && (remember(d._id), remember(d._id.slice(prefix.length)));
548
+ break;
549
+ }
550
+ case "sanity.action.document.version.create": {
551
+ const releaseId = a.releaseId ?? (typeof a.document._id == "string" ? parseVersionId(a.document._id).releaseId : void 0);
552
+ releaseId !== void 0 && remember(versionDocId(releaseId, a.publishedId));
553
+ break;
554
+ }
555
+ case "sanity.action.document.version.replace":
556
+ typeof a.document._id == "string" && remember(a.document._id);
557
+ break;
558
+ case "sanity.action.document.version.discard":
559
+ case "sanity.action.document.version.unpublish":
560
+ remember(a.versionId);
561
+ break;
562
+ }
563
+ last = applyAction(store, a);
564
+ }
565
+ return last;
566
+ } catch (err) {
567
+ for (const [id, doc] of snapshot)
568
+ doc === void 0 ? store.remove(id) : store.put(doc);
569
+ throw err;
570
+ }
293
571
  }
294
- function cryptoRandomKey(length = 12) {
295
- const bytes = new Uint8Array(length);
296
- return globalThis.crypto.getRandomValues(bytes), [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, length);
572
+ const DRAFTS_PREFIX = "drafts.", VERSIONS_PREFIX = "versions.", RELEASE_DOC_PREFIX = "_.releases.";
573
+ function publishedIdOf(id) {
574
+ if (id.startsWith(DRAFTS_PREFIX)) return id.slice(DRAFTS_PREFIX.length);
575
+ if (id.startsWith(VERSIONS_PREFIX)) {
576
+ const rest = id.slice(VERSIONS_PREFIX.length), dot = rest.indexOf(".");
577
+ return dot === -1 ? rest : rest.slice(dot + 1);
578
+ }
579
+ return id;
580
+ }
581
+ function releaseNameOf(id) {
582
+ if (!id.startsWith(VERSIONS_PREFIX)) return;
583
+ const rest = id.slice(VERSIONS_PREFIX.length), dot = rest.indexOf(".");
584
+ if (dot !== -1)
585
+ return rest.slice(0, dot);
586
+ }
587
+ const isDraftId = (id) => id.startsWith(DRAFTS_PREFIX), isVersionId = (id) => id.startsWith(VERSIONS_PREFIX), isSystemId = (id) => id.startsWith("_."), isReleaseDocId = (id) => id.startsWith(RELEASE_DOC_PREFIX);
588
+ function applyPerspective(docs, perspective) {
589
+ if (perspective === "raw") return [...docs];
590
+ if (perspective === "published")
591
+ return docs.filter((d) => !isDraftId(d._id) && !isVersionId(d._id) && !isSystemId(d._id));
592
+ if (perspective === "drafts")
593
+ return mergeStack(docs, [], { includeDrafts: !0, includeReleaseDocs: !1 });
594
+ const stack = [];
595
+ let includeDrafts = !1;
596
+ for (const entry of perspective)
597
+ entry === "drafts" ? includeDrafts = !0 : stack.push(entry);
598
+ return mergeStack(docs, stack, { includeDrafts, includeReleaseDocs: !0 });
599
+ }
600
+ function mergeStack(docs, releaseStack, options) {
601
+ const groups = /* @__PURE__ */ new Map(), systemReleases = [];
602
+ for (const doc of docs) {
603
+ if (isReleaseDocId(doc._id)) {
604
+ options.includeReleaseDocs && systemReleases.push(doc);
605
+ continue;
606
+ }
607
+ if (isSystemId(doc._id)) continue;
608
+ const pubId = publishedIdOf(doc._id);
609
+ let group = groups.get(pubId);
610
+ if (group === void 0 && (group = { versions: /* @__PURE__ */ new Map() }, groups.set(pubId, group)), isDraftId(doc._id))
611
+ group.draft = doc;
612
+ else if (isVersionId(doc._id)) {
613
+ const releaseName = releaseNameOf(doc._id);
614
+ releaseName !== void 0 && group.versions.set(releaseName, doc);
615
+ } else
616
+ group.published = doc;
617
+ }
618
+ const out = [];
619
+ for (const [pubId, group] of groups) {
620
+ let chosen;
621
+ for (const releaseName of releaseStack) {
622
+ const v = group.versions.get(releaseName);
623
+ if (v !== void 0) {
624
+ if (isUnpublishStub(v)) {
625
+ chosen = void 0;
626
+ break;
627
+ }
628
+ chosen = v;
629
+ break;
630
+ }
631
+ }
632
+ chosen === void 0 && options.includeDrafts && group.draft !== void 0 && (chosen = group.draft), chosen === void 0 && group.published !== void 0 && (releaseStack.some((name) => {
633
+ const v = group.versions.get(name);
634
+ return v !== void 0 && isUnpublishStub(v);
635
+ }) || (chosen = group.published)), chosen !== void 0 && out.push(projectToPublishedId(chosen, pubId));
636
+ }
637
+ return [...out, ...systemReleases];
638
+ }
639
+ function isUnpublishStub(doc) {
640
+ return doc._system?.delete === !0;
641
+ }
642
+ function projectToPublishedId(doc, pubId) {
643
+ return doc._id === pubId ? doc : { ...doc, _id: pubId, _originalId: doc._id };
644
+ }
645
+ function resolveDocument(docs, id, perspective) {
646
+ return perspective === "raw" ? docs.find((d) => d._id === id) : applyPerspective(docs, perspective).find((d) => d._id === id);
297
647
  }
298
648
  function createTransaction(executor) {
299
649
  const mutations = [], handle = {
@@ -325,12 +675,193 @@ function createTransaction(executor) {
325
675
  };
326
676
  return handle;
327
677
  }
328
- const DEFAULT_PROJECT_ID = "test", DEFAULT_DATASET = "test", registryKey = (projectId, dataset) => `${projectId}:${dataset}`, parseRegistryKey = (key) => {
678
+ const ISSUES_URL = "https://github.com/sanity-labs/client-fake-for-test/issues", SUPPORTED_SURFACE = [
679
+ "fetch",
680
+ "getDocument",
681
+ "getDocuments",
682
+ "create",
683
+ "createOrReplace",
684
+ "createIfNotExists",
685
+ "delete",
686
+ "patch",
687
+ "transaction",
688
+ "mutate",
689
+ "action",
690
+ "releases",
691
+ "createVersion",
692
+ "unpublishVersion",
693
+ "request (only with accessControl)",
694
+ "config",
695
+ "withConfig"
696
+ ], UNSUPPORTED_MEMBERS = {
697
+ listen: {
698
+ what: "real-time listener (returns an RxJS Observable of mutation events)",
699
+ workaround: 'Test your subscription logic in isolation, or register a fake: client.stub("listen", () => myFakeObservable).'
700
+ },
701
+ observable: {
702
+ what: "the RxJS-flavoured mirror of the client API",
703
+ workaround: "Use the Promise-based methods and wrap them with `from()` if you need an Observable."
704
+ },
705
+ clone: {
706
+ what: "clone the client with the same config",
707
+ workaround: "Use `withConfig({})` \u2014 it returns a sibling handle into the same store."
708
+ },
709
+ getUrl: {
710
+ what: "build an absolute API URL for a path",
711
+ workaround: "Hard-code the expected URL in your test, or stub it."
712
+ },
713
+ getDataUrl: {
714
+ what: "build an absolute data-API URL",
715
+ workaround: "Hard-code the expected URL in your test, or stub it."
716
+ },
717
+ assets: {
718
+ what: "asset upload / metadata namespace",
719
+ workaround: "Seed asset documents into the store directly with `seed()` / `add()`."
720
+ },
721
+ agent: {
722
+ what: "Agent Actions namespace (generate / transform / \u2026)",
723
+ workaround: 'Stub the call site: client.stub("agent", myFakeAgent).'
724
+ },
725
+ projects: {
726
+ what: "project-management namespace",
727
+ workaround: "Out of scope for a data-plane fake \u2014 stub the call site."
728
+ },
729
+ users: {
730
+ what: "user-management namespace",
731
+ workaround: "Out of scope for a data-plane fake \u2014 stub the call site."
732
+ },
733
+ live: {
734
+ what: "Live Content API namespace",
735
+ workaround: "Out of scope \u2014 stub the call site or test the consumer separately."
736
+ },
737
+ mediaLibrary: {
738
+ what: "Media Library namespace",
739
+ workaround: "Out of scope \u2014 seed documents directly or stub the call site."
740
+ },
741
+ request: {
742
+ what: "generic HTTP request hook \u2014 only wired up when accessControl is configured",
743
+ workaround: "Create the client with `accessControl` to enable request() (it serves /users/me + the ACL path), or register a stub."
744
+ },
745
+ dryRun: {
746
+ what: "not a method \u2014 did you mean the `{ dryRun }` mutation option?",
747
+ workaround: "Pass `{ dryRun: true }` as the second arg to create / mutate / delete."
748
+ }
749
+ }, PROBE_NAMES = /* @__PURE__ */ new Set([
750
+ "then",
751
+ "catch",
752
+ "finally",
753
+ "toJSON",
754
+ "asymmetricMatch",
755
+ "$$typeof",
756
+ "nodeType",
757
+ "_isMockFunction"
758
+ ]);
759
+ function guardMessage(path, detail, workaround) {
760
+ return [
761
+ `[client-fake-for-test] \`client.${path}\` ${detail}`,
762
+ "",
763
+ "This is an in-memory fake of @sanity/client. It implements:",
764
+ ` ${SUPPORTED_SURFACE.join(", ")}.`,
765
+ "",
766
+ `\u2192 ${workaround}`,
767
+ "",
768
+ `Register a stub to fill any gap: client.stub(${JSON.stringify(path.split(".")[0])}, handler)`,
769
+ `If this should be supported, open an issue: ${ISSUES_URL}`
770
+ ].join(`
771
+ `);
772
+ }
773
+ function makeGuard(path, message) {
774
+ const thrower = () => {
775
+ throw new Error(message);
776
+ };
777
+ return new Proxy(thrower, {
778
+ apply: () => thrower(),
779
+ get: (_t, prop) => {
780
+ if (typeof prop == "symbol" || PROBE_NAMES.has(prop)) return;
781
+ const childPath = `${path}.${String(prop)}`, top = path.split(".")[0], entry = UNSUPPORTED_MEMBERS[top], detail = entry ? `is not implemented (${entry.what}).` : "is not implemented.", workaround = entry?.workaround ?? "Register a stub or use a different test strategy.";
782
+ return makeGuard(childPath, guardMessage(childPath, detail, workaround));
783
+ }
784
+ });
785
+ }
786
+ function wrapRecorded(method, fn, onCall) {
787
+ return (...args) => {
788
+ const settle = onCall(method, args);
789
+ try {
790
+ const out = fn(...args);
791
+ return out !== null && typeof out == "object" && typeof out.then == "function" ? out.then(
792
+ (result) => (settle({ result }), result),
793
+ (error) => {
794
+ throw settle({ error }), error;
795
+ }
796
+ ) : (settle({ result: out }), out);
797
+ } catch (error) {
798
+ throw settle({ error }), error;
799
+ }
800
+ };
801
+ }
802
+ function wrapNamespace(ns, prefix, onCall) {
803
+ return new Proxy(ns, {
804
+ get(target, prop, receiver) {
805
+ const value = Reflect.get(target, prop, receiver);
806
+ return typeof prop == "string" && typeof value == "function" ? wrapRecorded(`${prefix}.${prop}`, value, onCall) : value;
807
+ }
808
+ });
809
+ }
810
+ function wrapWithGuards(client, config) {
811
+ const { getStub, onCall, recorded } = config;
812
+ return new Proxy(client, {
813
+ get(target, prop, receiver) {
814
+ if (typeof prop == "symbol") return Reflect.get(target, prop, receiver);
815
+ if (prop in target) {
816
+ const value = Reflect.get(target, prop, receiver);
817
+ if (onCall !== void 0 && recorded?.has(prop) === !0) {
818
+ if (typeof value == "function")
819
+ return wrapRecorded(prop, value, onCall);
820
+ if (value !== null && typeof value == "object")
821
+ return wrapNamespace(value, prop, onCall);
822
+ }
823
+ return value;
824
+ }
825
+ if (PROBE_NAMES.has(prop)) return;
826
+ const stub = getStub(prop);
827
+ if (stub !== void 0) return stub;
828
+ const entry = UNSUPPORTED_MEMBERS[prop];
829
+ return entry !== void 0 ? makeGuard(
830
+ prop,
831
+ guardMessage(prop, `is not implemented (${entry.what}).`, entry.workaround)
832
+ ) : makeGuard(
833
+ prop,
834
+ guardMessage(
835
+ prop,
836
+ "is not a method on this fake (unknown member or unimplemented @sanity/client API).",
837
+ "Check for a typo, register a stub, or use a different test strategy."
838
+ )
839
+ );
840
+ }
841
+ });
842
+ }
843
+ const RECORDED_METHODS = /* @__PURE__ */ new Set([
844
+ "fetch",
845
+ "getDocument",
846
+ "getDocuments",
847
+ "create",
848
+ "createOrReplace",
849
+ "createIfNotExists",
850
+ "delete",
851
+ "patch",
852
+ "transaction",
853
+ "mutate",
854
+ "action",
855
+ "releases",
856
+ "createVersion",
857
+ "unpublishVersion",
858
+ "request"
859
+ ]), DEFAULT_PROJECT_ID = "test", DEFAULT_DATASET = "test", registryKey = (projectId, dataset) => `${projectId}:${dataset}`, parseRegistryKey = (key) => {
329
860
  const [projectId, dataset] = key.split(":");
330
861
  return { projectId, dataset };
331
862
  };
332
863
  function createTestClient(options = {}) {
333
- const defaultProjectId = options.projectId ?? DEFAULT_PROJECT_ID, defaultDataset = options.dataset ?? DEFAULT_DATASET, registry = /* @__PURE__ */ new Map(), ensureStore = (projectId, dataset) => {
864
+ const defaultProjectId = options.projectId ?? DEFAULT_PROJECT_ID, defaultDataset = options.dataset ?? DEFAULT_DATASET, backend = { registry: /* @__PURE__ */ new Map(), log: [], stubs: /* @__PURE__ */ new Map(), calls: [] }, registry = backend.registry, ensureStore = (projectId, dataset) => {
334
865
  const key = registryKey(projectId, dataset);
335
866
  let store = registry.get(key);
336
867
  return store === void 0 && (store = createTestStore(), registry.set(key, store)), store;
@@ -340,19 +871,87 @@ function createTestClient(options = {}) {
340
871
  const { projectId, dataset } = parseRegistryKey(key);
341
872
  ensureStore(projectId, dataset).seed(docs);
342
873
  }
343
- return makeHandle(registry, defaultProjectId, defaultDataset, options.accessControl);
874
+ return makeHandle(
875
+ backend,
876
+ defaultProjectId,
877
+ defaultDataset,
878
+ options.accessControl,
879
+ options.perspective ?? "raw"
880
+ );
881
+ }
882
+ function dedupeIds(results) {
883
+ const seen = /* @__PURE__ */ new Set(), out = [];
884
+ for (const r of results)
885
+ seen.has(r.id) || (seen.add(r.id), out.push(r.id));
886
+ return out;
344
887
  }
345
- function makeHandle(registry, projectId, dataset, accessControl) {
346
- const store = (() => {
888
+ function formatMutationResult(documents, results, transactionId, returnDocuments, returnFirst) {
889
+ return returnDocuments ? returnFirst ? documents[0] : documents : returnFirst ? {
890
+ transactionId,
891
+ documentId: results[0]?.id,
892
+ results
893
+ } : {
894
+ transactionId,
895
+ documentIds: dedupeIds(results),
896
+ results
897
+ };
898
+ }
899
+ function toSingleTxnMutation(op) {
900
+ if ("create" in op) return { kind: "create", doc: op.create };
901
+ if ("createOrReplace" in op) return { kind: "createOrReplace", doc: op.createOrReplace };
902
+ if ("createIfNotExists" in op) return { kind: "createIfNotExists", doc: op.createIfNotExists };
903
+ if ("delete" in op) return { kind: "delete", id: op.delete.id };
904
+ const p = op.patch, ops = [];
905
+ if (p.set !== void 0 && ops.push({ kind: "set", props: p.set }), p.setIfMissing !== void 0 && ops.push({ kind: "setIfMissing", props: p.setIfMissing }), p.unset !== void 0 && ops.push({ kind: "unset", paths: p.unset }), p.inc !== void 0 && ops.push({ kind: "inc", props: p.inc }), p.dec !== void 0 && ops.push({ kind: "dec", props: p.dec }), p.insert !== void 0) {
906
+ const { before, after, replace, items } = p.insert, position = before !== void 0 ? "before" : after !== void 0 ? "after" : "replace", anchor = before ?? after ?? replace;
907
+ if (anchor === void 0)
908
+ throw fakeError(
909
+ 'mutate(): a `patch.insert` operation must name exactly one anchor \u2014 `before`, `after`, or `replace` \u2014 pointing at an array path (e.g. "tags[-1]" or `tags[_key=="x"]`), alongside `items`. None was set.'
910
+ );
911
+ ops.push({ kind: "insert", position, anchor, items });
912
+ }
913
+ return {
914
+ kind: "patch",
915
+ documentId: p.id,
916
+ ops,
917
+ ...p.ifRevisionID !== void 0 ? { ifRev: p.ifRevisionID } : {}
918
+ };
919
+ }
920
+ function toTxnMutations(operations) {
921
+ if (Array.isArray(operations)) return operations.map(toSingleTxnMutation);
922
+ const json = operations.toJSON();
923
+ return Array.isArray(json) ? json : [
924
+ {
925
+ kind: "patch",
926
+ documentId: json.id,
927
+ ops: json.ops,
928
+ ...json.ifRev !== void 0 ? { ifRev: json.ifRev } : {}
929
+ }
930
+ ];
931
+ }
932
+ function makeHandle(backend, projectId, dataset, accessControl, perspective) {
933
+ const { registry } = backend, store = (() => {
347
934
  const key = registryKey(projectId, dataset);
348
935
  let s = registry.get(key);
349
936
  return s === void 0 && (s = createTestStore(), registry.set(key, s)), s;
350
- })(), promote = (doc) => {
937
+ })(), record = (transactionId, mutations) => {
938
+ backend.log.push({ transactionId, projectId, dataset, mutations });
939
+ }, writeResult = (documents, results, transactionId, opts) => formatMutationResult(
940
+ documents,
941
+ results,
942
+ transactionId,
943
+ opts?.returnDocuments ?? !0,
944
+ opts?.returnFirst ?? !0
945
+ ), promote2 = (doc) => {
351
946
  const now = (/* @__PURE__ */ new Date()).toISOString();
352
947
  if (typeof doc._id != "string" || doc._id.length === 0)
353
- throw new Error("document is missing _id");
948
+ throw fakeError(
949
+ `Cannot store a document without a non-empty string \`_id\` (got ${JSON.stringify(doc._id)}). Unlike @sanity/client, this fake never auto-generates ids \u2014 give every document an explicit _id.`
950
+ );
354
951
  if (typeof doc._type != "string" || doc._type.length === 0)
355
- throw new Error(`document ${doc._id} is missing _type`);
952
+ throw fakeError(
953
+ `Document "${doc._id}" is missing a non-empty string \`_type\` (got ${JSON.stringify(doc._type)}). Every Sanity document needs a _type.`
954
+ );
356
955
  return {
357
956
  ...doc,
358
957
  _id: doc._id,
@@ -365,10 +964,12 @@ function makeHandle(registry, projectId, dataset, accessControl) {
365
964
  if (ifRevisionId === void 0) return;
366
965
  const existing = store.get(id);
367
966
  if (existing === void 0)
368
- throw new Error(`ifRevisionId check failed: document ${id} does not exist`);
967
+ throw fakeError(
968
+ `ifRevisionId check failed: document "${id}" does not exist, so its revision cannot match the expected _rev="${ifRevisionId}". Drop ifRevisionId when creating a new document, or seed/create the document before guarding on its revision.`
969
+ );
369
970
  if (existing._rev !== ifRevisionId)
370
- throw new Error(
371
- `ifRevisionId check failed: document ${id} has _rev=${existing._rev}, expected ${ifRevisionId}`
971
+ throw fakeError(
972
+ `ifRevisionId check failed: document "${id}" is at _rev="${existing._rev}" but the mutation required _rev="${ifRevisionId}". The document changed since you read it \u2014 re-read it and retry. (This mirrors Sanity's optimistic-locking 409.)`
372
973
  );
373
974
  }, gate = async (document, permission) => {
374
975
  accessControl !== void 0 && await enforceWriteAccess({
@@ -385,7 +986,9 @@ function makeHandle(registry, projectId, dataset, accessControl) {
385
986
  }), applyPatch = async (documentId, ops, ifRev) => {
386
987
  const existing = store.get(documentId);
387
988
  if (existing === void 0)
388
- throw new Error(`Cannot patch document ${documentId}: not found`);
989
+ throw fakeError(
990
+ `Cannot patch document "${documentId}": not found \u2014 no document with that _id exists in dataset "${projectId}:${dataset}". patch() requires the target to already exist; use createOrReplace() / createIfNotExists() to upsert, or seed the document first.`
991
+ );
389
992
  checkRevision(documentId, ifRev), await gate(existing, "update");
390
993
  const patched = applyPatchOps(existing, ops), now = (/* @__PURE__ */ new Date()).toISOString(), next = {
391
994
  ...patched,
@@ -394,7 +997,7 @@ function makeHandle(registry, projectId, dataset, accessControl) {
394
997
  };
395
998
  return store.put(next), next;
396
999
  }, applyTransaction = async (mutations, txnOptions) => {
397
- const snapshot = /* @__PURE__ */ new Map(), touchedIds = [], operations = [], remember = (id) => {
1000
+ const transactionId = txnOptions?.transactionId ?? cryptoRandomKey(), snapshot = /* @__PURE__ */ new Map(), touchedIds = [], operations = [], remember = (id) => {
398
1001
  snapshot.has(id) || snapshot.set(id, store.get(id));
399
1002
  };
400
1003
  try {
@@ -402,15 +1005,17 @@ function makeHandle(registry, projectId, dataset, accessControl) {
402
1005
  switch (mut.kind) {
403
1006
  case "create": {
404
1007
  remember(mut.doc._id);
405
- const promoted = promote(mut.doc);
1008
+ const promoted = promote2(mut.doc);
406
1009
  if (store.get(promoted._id) !== void 0)
407
- throw new Error(`create failed: document ${promoted._id} already exists`);
1010
+ throw fakeError(
1011
+ `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
+ );
408
1013
  await gate(promoted, "create"), store.put(promoted), touchedIds.push(promoted._id), operations.push({ id: promoted._id, operation: "create" });
409
1014
  break;
410
1015
  }
411
1016
  case "createOrReplace": {
412
1017
  remember(mut.doc._id);
413
- const promoted = promote(mut.doc), existing = store.get(promoted._id);
1018
+ const promoted = promote2(mut.doc), existing = store.get(promoted._id);
414
1019
  await gate(existing ?? promoted, existing !== void 0 ? "update" : "create");
415
1020
  const wasPresent = existing !== void 0;
416
1021
  store.put(promoted), touchedIds.push(promoted._id), operations.push({ id: promoted._id, operation: wasPresent ? "update" : "create" });
@@ -418,7 +1023,7 @@ function makeHandle(registry, projectId, dataset, accessControl) {
418
1023
  }
419
1024
  case "createIfNotExists": {
420
1025
  if (remember(mut.doc._id), store.get(mut.doc._id) === void 0) {
421
- const promoted = promote(mut.doc);
1026
+ const promoted = promote2(mut.doc);
422
1027
  await gate(promoted, "create"), store.put(promoted), touchedIds.push(promoted._id), operations.push({ id: promoted._id, operation: "create" });
423
1028
  }
424
1029
  break;
@@ -439,84 +1044,231 @@ function makeHandle(registry, projectId, dataset, accessControl) {
439
1044
  doc === void 0 ? store.remove(id) : store.put(doc);
440
1045
  throw error;
441
1046
  }
442
- if (txnOptions?.returnDocuments) {
443
- const docs = touchedIds.map((id) => store.get(id)).filter((d) => d !== void 0);
444
- return txnOptions.returnFirst ? docs[0] : docs;
445
- }
446
- return {
447
- transactionId: cryptoRandomKey(),
448
- results: operations
449
- };
450
- }, visibleDocs = async () => {
1047
+ record(transactionId, mutations);
1048
+ const documents = touchedIds.map((id) => store.get(id)).filter((d) => d !== void 0);
1049
+ return formatMutationResult(
1050
+ documents,
1051
+ operations,
1052
+ transactionId,
1053
+ txnOptions?.returnDocuments ?? !1,
1054
+ txnOptions?.returnFirst ?? !1
1055
+ );
1056
+ }, accessVisibleDocs = async () => {
451
1057
  const all = store.all();
452
1058
  if (accessControl === void 0) return all;
453
1059
  const out = [];
454
1060
  for (const doc of all)
455
1061
  await canRead(doc) && out.push(doc);
456
1062
  return out;
457
- };
458
- return {
459
- fetch: async (query, params) => {
460
- const tree = groqJs.parse(query, params ? { params } : void 0);
461
- return await (await groqJs.evaluate(tree, {
462
- dataset: await visibleDocs(),
1063
+ }, visibleDocs = async (override) => {
1064
+ const accessible = await accessVisibleDocs();
1065
+ return applyPerspective(accessible, override ?? perspective);
1066
+ }, client = {
1067
+ fetch: async (query, params, opts) => {
1068
+ const start = Date.now(), tree = groqJs.parse(query, params ? { params } : void 0), result = await (await groqJs.evaluate(tree, {
1069
+ dataset: await visibleDocs(opts?.perspective),
463
1070
  ...params !== void 0 ? { params } : {}
464
1071
  })).get();
1072
+ if (opts?.filterResponse === !1) {
1073
+ const ms = Date.now() - start;
1074
+ return opts.returnQuery === !1 ? { ms, result } : { query, ms, result };
1075
+ }
1076
+ return result;
465
1077
  },
466
- getDocument: async (id) => {
467
- const doc = store.get(id);
468
- return doc === void 0 || !await canRead(doc) ? null : doc;
469
- },
470
- getDocuments: async (ids) => {
471
- const result = [];
472
- for (const id of ids) {
1078
+ getDocument: async (id, opts) => {
1079
+ const effective = opts?.perspective ?? perspective;
1080
+ if (effective === "raw") {
473
1081
  const doc = store.get(id);
474
- doc === void 0 ? result.push(null) : await canRead(doc) ? result.push(doc) : result.push(null);
1082
+ return doc === void 0 || !await canRead(doc) ? void 0 : doc;
475
1083
  }
476
- return result;
1084
+ const view = await visibleDocs(effective);
1085
+ return resolveDocument(view, id, effective) ?? void 0;
1086
+ },
1087
+ getDocuments: async (ids, opts) => {
1088
+ const effective = opts?.perspective ?? perspective;
1089
+ if (effective === "raw") {
1090
+ const result = [];
1091
+ for (const id of ids) {
1092
+ const doc = store.get(id);
1093
+ doc === void 0 ? result.push(null) : await canRead(doc) ? result.push(doc) : result.push(null);
1094
+ }
1095
+ return result;
1096
+ }
1097
+ const view = await visibleDocs(effective);
1098
+ return ids.map((id) => resolveDocument(view, id, effective) ?? null);
477
1099
  },
478
1100
  create: async (doc, opts) => {
479
1101
  if (checkRevision(doc._id, opts?.ifRevisionId), store.get(doc._id) !== void 0)
480
- throw new Error(`create failed: document ${doc._id} already exists`);
481
- const promoted = promote(doc);
482
- return await gate(promoted, "create"), store.put(promoted), promoted;
1102
+ throw fakeError(
1103
+ `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
+ );
1105
+ const promoted = promote2(doc);
1106
+ await gate(promoted, "create");
1107
+ const transactionId = opts?.transactionId ?? cryptoRandomKey();
1108
+ return opts?.dryRun !== !0 && (store.put(promoted), record(transactionId, [{ kind: "create", doc }])), writeResult(
1109
+ [promoted],
1110
+ [{ id: promoted._id, operation: "create" }],
1111
+ transactionId,
1112
+ opts
1113
+ );
483
1114
  },
484
1115
  createOrReplace: async (doc, opts) => {
485
1116
  checkRevision(doc._id, opts?.ifRevisionId);
486
- const promoted = promote(doc), existing = store.get(doc._id);
487
- return await gate(existing ?? promoted, existing !== void 0 ? "update" : "create"), store.put(promoted), promoted;
1117
+ const promoted = promote2(doc), existing = store.get(doc._id);
1118
+ await gate(existing ?? promoted, existing !== void 0 ? "update" : "create");
1119
+ const transactionId = opts?.transactionId ?? cryptoRandomKey();
1120
+ opts?.dryRun !== !0 && (store.put(promoted), record(transactionId, [{ kind: "createOrReplace", doc }]));
1121
+ const operation = existing !== void 0 ? "update" : "create";
1122
+ return writeResult(
1123
+ [promoted],
1124
+ [{ id: promoted._id, operation }],
1125
+ transactionId,
1126
+ opts
1127
+ );
488
1128
  },
489
1129
  createIfNotExists: async (doc, opts) => {
490
1130
  checkRevision(doc._id, opts?.ifRevisionId);
491
- const existing = store.get(doc._id);
492
- if (existing !== void 0) return existing;
493
- const promoted = promote(doc);
494
- return await gate(promoted, "create"), store.put(promoted), promoted;
1131
+ const existing = store.get(doc._id), transactionId = opts?.transactionId ?? cryptoRandomKey();
1132
+ if (existing !== void 0)
1133
+ return writeResult(
1134
+ [existing],
1135
+ [{ id: existing._id, operation: "none" }],
1136
+ transactionId,
1137
+ opts
1138
+ );
1139
+ const promoted = promote2(doc);
1140
+ return await gate(promoted, "create"), opts?.dryRun !== !0 && (store.put(promoted), record(transactionId, [{ kind: "createIfNotExists", doc }])), writeResult(
1141
+ [promoted],
1142
+ [{ id: promoted._id, operation: "create" }],
1143
+ transactionId,
1144
+ opts
1145
+ );
495
1146
  },
496
1147
  delete: async (id, opts) => {
497
1148
  checkRevision(id, opts?.ifRevisionId);
498
1149
  const existing = store.get(id);
499
- return existing !== void 0 && await gate(existing, "update"), store.remove(id) ? { _id: id } : null;
1150
+ existing !== void 0 && await gate(existing, "update");
1151
+ const transactionId = opts?.transactionId ?? cryptoRandomKey();
1152
+ let removed = existing;
1153
+ 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(
1154
+ removed !== void 0 ? [removed] : [],
1155
+ removed !== void 0 ? [{ id, operation: "delete" }] : [],
1156
+ transactionId,
1157
+ opts.returnDocuments ?? !0,
1158
+ opts.returnFirst ?? !0
1159
+ );
500
1160
  },
501
- patch: (documentId) => createPatch(documentId, { apply: applyPatch }),
1161
+ patch: (documentId) => createPatch(documentId, {
1162
+ apply: async (docId, ops, ifRev) => {
1163
+ const doc = await applyPatch(docId, ops, ifRev);
1164
+ return record(cryptoRandomKey(), [
1165
+ { kind: "patch", documentId: docId, ops, ...ifRev !== void 0 ? { ifRev } : {} }
1166
+ ]), doc;
1167
+ }
1168
+ }),
502
1169
  transaction: () => createTransaction({ apply: applyTransaction }),
1170
+ mutate: async (operations, options) => applyTransaction(toTxnMutations(operations), options),
1171
+ action: async (action) => applyActions(store, action),
1172
+ releases: {
1173
+ create: async ({ releaseId, metadata }) => {
1174
+ const result = applyActions(store, {
1175
+ actionType: "sanity.action.release.create",
1176
+ releaseId,
1177
+ ...metadata !== void 0 ? { metadata } : {}
1178
+ }), doc = store.get(`_.releases.${releaseId}`);
1179
+ return {
1180
+ ...result,
1181
+ releaseId,
1182
+ metadata: doc?.metadata ?? { releaseType: "undecided" }
1183
+ };
1184
+ },
1185
+ edit: async ({ releaseId, patch }) => applyActions(store, { actionType: "sanity.action.release.edit", releaseId, patch }),
1186
+ get: async ({ releaseId }) => store.get(`_.releases.${releaseId}`),
1187
+ fetchDocuments: async ({ releaseId }) => {
1188
+ const prefix = `versions.${releaseId}.`;
1189
+ return store.all().filter((d) => d._id.startsWith(prefix));
1190
+ },
1191
+ schedule: async ({ releaseId, publishAt }) => applyActions(store, {
1192
+ actionType: "sanity.action.release.schedule",
1193
+ releaseId,
1194
+ publishAt
1195
+ }),
1196
+ unschedule: async ({ releaseId }) => applyActions(store, { actionType: "sanity.action.release.unschedule", releaseId }),
1197
+ publish: async ({ releaseId }) => applyActions(store, { actionType: "sanity.action.release.publish", releaseId }),
1198
+ archive: async ({ releaseId }) => applyActions(store, { actionType: "sanity.action.release.archive", releaseId }),
1199
+ unarchive: async ({ releaseId }) => applyActions(store, { actionType: "sanity.action.release.unarchive", releaseId }),
1200
+ delete: async ({ releaseId }) => applyActions(store, { actionType: "sanity.action.release.delete", releaseId })
1201
+ },
1202
+ createVersion: async ({ releaseId, publishedId, document }) => applyActions(store, {
1203
+ actionType: "sanity.action.document.version.create",
1204
+ releaseId,
1205
+ publishedId,
1206
+ document
1207
+ }),
1208
+ unpublishVersion: async ({ releaseId, publishedId }) => applyActions(store, {
1209
+ actionType: "sanity.action.document.version.unpublish",
1210
+ versionId: `versions.${releaseId}.${publishedId}`,
1211
+ publishedId
1212
+ }),
503
1213
  ...accessControl !== void 0 ? { request: makeRequestImpl(accessControl, projectId, dataset) } : {},
504
- config: () => ({ projectId, dataset }),
505
- withConfig: (config) => makeHandle(registry, config.projectId ?? projectId, config.dataset ?? dataset, accessControl),
1214
+ config: () => ({ projectId, dataset, perspective }),
1215
+ withConfig: (config) => makeHandle(
1216
+ backend,
1217
+ config.projectId ?? projectId,
1218
+ config.dataset ?? dataset,
1219
+ accessControl,
1220
+ config.perspective ?? perspective
1221
+ ),
506
1222
  seed: (documents) => store.seed(documents),
507
1223
  add: (documents) => store.add(documents),
508
1224
  reset: () => store.reset(),
509
1225
  snapshot: () => store.snapshot(),
510
1226
  store: () => store,
511
- datasets: () => [...registry.keys()].map(parseRegistryKey)
1227
+ datasets: () => [...registry.keys()].map(parseRegistryKey),
1228
+ stub: (name, handler) => {
1229
+ backend.stubs.set(name, handler);
1230
+ },
1231
+ stubs: () => Object.fromEntries(backend.stubs),
1232
+ mutations: () => backend.log.flatMap((entry) => entry.mutations),
1233
+ transactions: () => [...backend.log],
1234
+ clearLog: () => {
1235
+ backend.log.length = 0;
1236
+ },
1237
+ calls: (method) => method === void 0 ? [...backend.calls] : backend.calls.filter((c) => c.method === method),
1238
+ callCount: (method) => method === void 0 ? backend.calls.length : backend.calls.filter((c) => c.method === method).length,
1239
+ clearCalls: () => {
1240
+ backend.calls.length = 0;
1241
+ }
512
1242
  };
1243
+ return wrapWithGuards(client, {
1244
+ getStub: (name) => backend.stubs.get(name),
1245
+ onCall: (method, args) => {
1246
+ const rec = { method, args, projectId, dataset };
1247
+ return backend.calls.push(rec), (outcome) => {
1248
+ "error" in outcome ? rec.error = outcome.error : rec.result = outcome.result;
1249
+ };
1250
+ },
1251
+ recorded: RECORDED_METHODS
1252
+ });
513
1253
  }
1254
+ exports.ERR_PREFIX = ERR_PREFIX;
1255
+ exports.SUPPORTED_SURFACE = SUPPORTED_SURFACE;
1256
+ exports.UNSUPPORTED_MEMBERS = UNSUPPORTED_MEMBERS;
514
1257
  exports.WriteAccessDeniedError = WriteAccessDeniedError;
515
1258
  exports.applyPatchOps = applyPatchOps;
1259
+ exports.applyPerspective = applyPerspective;
516
1260
  exports.createPatch = createPatch;
517
1261
  exports.createTestClient = createTestClient;
518
1262
  exports.createTestStore = createTestStore;
519
1263
  exports.createTransaction = createTransaction;
520
1264
  exports.defaultAclPath = defaultAclPath;
521
1265
  exports.evaluateWriteAccess = evaluateWriteAccess;
1266
+ exports.fakeError = fakeError;
1267
+ exports.isDraftId = isDraftId;
1268
+ exports.isReleaseDocId = isReleaseDocId;
1269
+ exports.isSystemId = isSystemId;
1270
+ exports.isVersionId = isVersionId;
1271
+ exports.publishedIdOf = publishedIdOf;
1272
+ exports.releaseNameOf = releaseNameOf;
1273
+ exports.wrapWithGuards = wrapWithGuards;
522
1274
  //# sourceMappingURL=index.cjs.map