@sanity-labs/client-fake-for-test 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +141 -26
- package/dist/index.cjs +466 -76
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +357 -10
- package/dist/index.d.ts +357 -10
- package/dist/index.js +467 -77
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { parse, evaluate } from "groq-js";
|
|
2
|
+
const ERR_PREFIX = "[client-fake-for-test]";
|
|
3
|
+
function fakeError(message) {
|
|
4
|
+
return new Error(`${ERR_PREFIX} ${message}`);
|
|
5
|
+
}
|
|
2
6
|
class WriteAccessDeniedError extends Error {
|
|
3
7
|
documentId;
|
|
4
8
|
permission;
|
|
@@ -63,17 +67,21 @@ function makeRequestImpl(accessControl, projectId, dataset) {
|
|
|
63
67
|
return accessControl.currentUser;
|
|
64
68
|
if (path === aclPath)
|
|
65
69
|
return accessControl.grants;
|
|
66
|
-
throw
|
|
67
|
-
`
|
|
70
|
+
throw fakeError(
|
|
71
|
+
`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.`
|
|
68
72
|
);
|
|
69
73
|
};
|
|
70
74
|
}
|
|
71
75
|
function createTestStore(initial) {
|
|
72
76
|
const docs = /* @__PURE__ */ new Map(), promote2 = (doc) => {
|
|
73
77
|
if (typeof doc._id != "string" || doc._id.length === 0)
|
|
74
|
-
throw
|
|
78
|
+
throw fakeError(
|
|
79
|
+
`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)}`
|
|
80
|
+
);
|
|
75
81
|
if (typeof doc._type != "string" || doc._type.length === 0)
|
|
76
|
-
throw
|
|
82
|
+
throw fakeError(
|
|
83
|
+
`Document "${doc._id}" is missing a non-empty string \`_type\` (got ${JSON.stringify(doc._type)}). Every Sanity document needs a _type.`
|
|
84
|
+
);
|
|
77
85
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
78
86
|
return {
|
|
79
87
|
...doc,
|
|
@@ -115,6 +123,9 @@ function cryptoRandomKey(length = 12) {
|
|
|
115
123
|
const bytes = new Uint8Array(length);
|
|
116
124
|
return globalThis.crypto.getRandomValues(bytes), [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, length);
|
|
117
125
|
}
|
|
126
|
+
function describeType(value) {
|
|
127
|
+
return value === null ? "null" : Array.isArray(value) ? "an array" : `a ${typeof value}`;
|
|
128
|
+
}
|
|
118
129
|
function createPatch(documentId, executor) {
|
|
119
130
|
const ops = [];
|
|
120
131
|
let ifRev;
|
|
@@ -175,7 +186,10 @@ function parsePath(path) {
|
|
|
175
186
|
const segments = [];
|
|
176
187
|
for (const raw of path.split(".")) {
|
|
177
188
|
const match = /^([A-Za-z_$][\w$]*)(?:\[(.+)\])?$/.exec(raw);
|
|
178
|
-
if (!match)
|
|
189
|
+
if (!match)
|
|
190
|
+
throw fakeError(
|
|
191
|
+
`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.`
|
|
192
|
+
);
|
|
179
193
|
const key = match[1], selectorRaw = match[2];
|
|
180
194
|
if (selectorRaw === void 0) {
|
|
181
195
|
segments.push({ key });
|
|
@@ -194,7 +208,9 @@ function parsePath(path) {
|
|
|
194
208
|
});
|
|
195
209
|
continue;
|
|
196
210
|
}
|
|
197
|
-
throw
|
|
211
|
+
throw fakeError(
|
|
212
|
+
`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.`
|
|
213
|
+
);
|
|
198
214
|
}
|
|
199
215
|
return segments;
|
|
200
216
|
}
|
|
@@ -204,7 +220,9 @@ function resolveSelector(array, selector) {
|
|
|
204
220
|
(entry) => typeof entry == "object" && entry !== null && entry[selector.field] === selector.value
|
|
205
221
|
);
|
|
206
222
|
if (idx === -1)
|
|
207
|
-
throw
|
|
223
|
+
throw fakeError(
|
|
224
|
+
`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).`
|
|
225
|
+
);
|
|
208
226
|
return idx;
|
|
209
227
|
}
|
|
210
228
|
function getAtPath(doc, path) {
|
|
@@ -222,14 +240,17 @@ function getAtPath(doc, path) {
|
|
|
222
240
|
}
|
|
223
241
|
function setAtPath(doc, path, value) {
|
|
224
242
|
const segments = parsePath(path);
|
|
225
|
-
if (segments.length === 0)
|
|
243
|
+
if (segments.length === 0)
|
|
244
|
+
throw fakeError("Empty patch path \u2014 a set/setIfMissing op needs a target field.");
|
|
226
245
|
let cursor = doc;
|
|
227
246
|
for (let i = 0; i < segments.length - 1; i++) {
|
|
228
247
|
const seg = segments[i];
|
|
229
248
|
let next = cursor[seg.key];
|
|
230
249
|
if (next === void 0 && (next = seg.selector?.kind === "index" || seg.selector?.kind === "keyed" ? [] : {}, cursor[seg.key] = next), seg.selector !== void 0) {
|
|
231
250
|
if (!Array.isArray(next))
|
|
232
|
-
throw
|
|
251
|
+
throw fakeError(
|
|
252
|
+
`Patch path "${path}" applies an array selector to field "${seg.key}", but that field holds ${describeType(next)}, not an array.`
|
|
253
|
+
);
|
|
233
254
|
const idx = resolveSelector(next, seg.selector);
|
|
234
255
|
cursor = next[idx];
|
|
235
256
|
} else
|
|
@@ -239,7 +260,9 @@ function setAtPath(doc, path, value) {
|
|
|
239
260
|
if (last.selector !== void 0) {
|
|
240
261
|
const arr = cursor[last.key];
|
|
241
262
|
if (!Array.isArray(arr))
|
|
242
|
-
throw
|
|
263
|
+
throw fakeError(
|
|
264
|
+
`Patch path "${path}" applies an array selector to field "${last.key}", but that field holds ${describeType(arr)}, not an array.`
|
|
265
|
+
);
|
|
243
266
|
const idx = resolveSelector(arr, last.selector);
|
|
244
267
|
arr[idx] = value;
|
|
245
268
|
} else
|
|
@@ -271,41 +294,56 @@ function unsetAtPath(doc, path) {
|
|
|
271
294
|
}
|
|
272
295
|
function insertIntoArray(doc, position, anchor, items) {
|
|
273
296
|
const segments = parsePath(anchor);
|
|
274
|
-
if (segments.length === 0)
|
|
297
|
+
if (segments.length === 0)
|
|
298
|
+
throw fakeError("Empty insert anchor \u2014 insert() needs an array path to anchor against.");
|
|
275
299
|
const last = segments[segments.length - 1];
|
|
276
300
|
if (last.selector === void 0)
|
|
277
|
-
throw
|
|
301
|
+
throw fakeError(
|
|
302
|
+
`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.`
|
|
303
|
+
);
|
|
278
304
|
let cursor = doc;
|
|
279
305
|
for (let i = 0; i < segments.length - 1; i++) {
|
|
280
306
|
const seg = segments[i], next = cursor[seg.key];
|
|
281
307
|
if (next == null)
|
|
282
|
-
throw
|
|
308
|
+
throw fakeError(
|
|
309
|
+
`Insert anchor "${anchor}" traverses field "${seg.key}", which is missing on the document. setIfMissing the parent first, or anchor against an existing array.`
|
|
310
|
+
);
|
|
283
311
|
if (seg.selector !== void 0) {
|
|
284
312
|
if (!Array.isArray(next))
|
|
285
|
-
throw
|
|
313
|
+
throw fakeError(
|
|
314
|
+
`Insert anchor "${anchor}" applies an array selector to field "${seg.key}", but it holds ${describeType(next)}, not an array.`
|
|
315
|
+
);
|
|
286
316
|
cursor = next[resolveSelector(next, seg.selector)];
|
|
287
317
|
} else
|
|
288
318
|
cursor = next;
|
|
289
319
|
}
|
|
290
320
|
const arr = cursor[last.key];
|
|
291
321
|
if (!Array.isArray(arr))
|
|
292
|
-
throw
|
|
322
|
+
throw fakeError(
|
|
323
|
+
`Insert anchor "${anchor}" targets field "${last.key}", but it holds ${describeType(arr)}, not an array.`
|
|
324
|
+
);
|
|
293
325
|
const idx = resolveSelector(arr, last.selector);
|
|
294
326
|
position === "replace" ? arr.splice(idx, 1, ...items) : position === "before" ? arr.splice(idx, 0, ...items) : arr.splice(idx + 1, 0, ...items);
|
|
295
327
|
}
|
|
296
328
|
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}`;
|
|
297
329
|
function parseVersionId(versionId) {
|
|
298
330
|
if (!versionId.startsWith(VERSIONS_PREFIX$1))
|
|
299
|
-
throw
|
|
331
|
+
throw fakeError(
|
|
332
|
+
`"${versionId}" is not a version document id. Version ids look like "versions.<releaseId>.<publishedId>" (e.g. "versions.summer-drop.product-1").`
|
|
333
|
+
);
|
|
300
334
|
const rest = versionId.slice(VERSIONS_PREFIX$1.length), dot = rest.indexOf(".");
|
|
301
335
|
if (dot === -1)
|
|
302
|
-
throw
|
|
336
|
+
throw fakeError(
|
|
337
|
+
`"${versionId}" is not a version document id. Version ids look like "versions.<releaseId>.<publishedId>" (e.g. "versions.summer-drop.product-1").`
|
|
338
|
+
);
|
|
303
339
|
return { releaseId: rest.slice(0, dot), publishedId: rest.slice(dot + 1) };
|
|
304
340
|
}
|
|
305
341
|
function getRelease(store, releaseId) {
|
|
306
342
|
const doc = store.get(releaseDocId(releaseId));
|
|
307
343
|
if (doc === void 0)
|
|
308
|
-
throw
|
|
344
|
+
throw fakeError(
|
|
345
|
+
`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.`
|
|
346
|
+
);
|
|
309
347
|
return doc;
|
|
310
348
|
}
|
|
311
349
|
function maybeRelease(store, releaseId) {
|
|
@@ -335,15 +373,17 @@ function bumpRev(doc, patch) {
|
|
|
335
373
|
}
|
|
336
374
|
function requireActive(release) {
|
|
337
375
|
if (release.state !== "active")
|
|
338
|
-
throw
|
|
339
|
-
`Release ${release.name} is ${release.state}
|
|
376
|
+
throw fakeError(
|
|
377
|
+
`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.`
|
|
340
378
|
);
|
|
341
379
|
}
|
|
342
380
|
function applyAction(store, action) {
|
|
343
381
|
switch (action.actionType) {
|
|
344
382
|
case "sanity.action.release.create": {
|
|
345
383
|
if (maybeRelease(store, action.releaseId) !== void 0)
|
|
346
|
-
throw
|
|
384
|
+
throw fakeError(
|
|
385
|
+
`Release "${action.releaseId}" already exists. release.create expects a new releaseId; use release.edit to change its metadata, or pick a different releaseId.`
|
|
386
|
+
);
|
|
347
387
|
const now = nowIso(), metadata = {
|
|
348
388
|
releaseType: "undecided",
|
|
349
389
|
...action.metadata
|
|
@@ -368,7 +408,9 @@ function applyAction(store, action) {
|
|
|
368
408
|
case "sanity.action.release.schedule": {
|
|
369
409
|
const release = getRelease(store, action.releaseId);
|
|
370
410
|
if (release.state !== "active")
|
|
371
|
-
throw
|
|
411
|
+
throw fakeError(
|
|
412
|
+
`Cannot schedule release "${action.releaseId}": it is "${release.state}", and only "active" releases can be scheduled.`
|
|
413
|
+
);
|
|
372
414
|
return store.put(
|
|
373
415
|
bumpRev(release, {
|
|
374
416
|
state: "scheduled",
|
|
@@ -379,7 +421,9 @@ function applyAction(store, action) {
|
|
|
379
421
|
case "sanity.action.release.unschedule": {
|
|
380
422
|
const release = getRelease(store, action.releaseId);
|
|
381
423
|
if (release.state !== "scheduled")
|
|
382
|
-
throw
|
|
424
|
+
throw fakeError(
|
|
425
|
+
`Cannot unschedule release "${action.releaseId}": it is "${release.state}", and only "scheduled" releases can be unscheduled.`
|
|
426
|
+
);
|
|
383
427
|
const next = { ...release, state: "active" };
|
|
384
428
|
return delete next.publishAt, store.put(bumpRev(next, {})), { transactionId: cryptoRandomKey() };
|
|
385
429
|
}
|
|
@@ -388,7 +432,9 @@ function applyAction(store, action) {
|
|
|
388
432
|
if (release.state === "published")
|
|
389
433
|
return { transactionId: cryptoRandomKey() };
|
|
390
434
|
if (release.state !== "active" && release.state !== "scheduled")
|
|
391
|
-
throw
|
|
435
|
+
throw fakeError(
|
|
436
|
+
`Cannot publish release "${action.releaseId}": it is "${release.state}", and only "active" or "scheduled" releases can be published.`
|
|
437
|
+
);
|
|
392
438
|
const prefix = `${VERSIONS_PREFIX$1}${action.releaseId}.`, sources = store.all().filter((d) => d._id.startsWith(prefix));
|
|
393
439
|
for (const v of sources) {
|
|
394
440
|
const pubId = v._id.slice(prefix.length);
|
|
@@ -410,20 +456,24 @@ function applyAction(store, action) {
|
|
|
410
456
|
case "sanity.action.release.archive": {
|
|
411
457
|
const release = getRelease(store, action.releaseId);
|
|
412
458
|
if (release.state !== "active" && release.state !== "scheduled")
|
|
413
|
-
throw
|
|
459
|
+
throw fakeError(
|
|
460
|
+
`Cannot archive release "${action.releaseId}": it is "${release.state}", and only "active" or "scheduled" releases can be archived.`
|
|
461
|
+
);
|
|
414
462
|
return store.put(bumpRev(release, { state: "archived" })), { transactionId: cryptoRandomKey() };
|
|
415
463
|
}
|
|
416
464
|
case "sanity.action.release.unarchive": {
|
|
417
465
|
const release = getRelease(store, action.releaseId);
|
|
418
466
|
if (release.state !== "archived")
|
|
419
|
-
throw
|
|
467
|
+
throw fakeError(
|
|
468
|
+
`Cannot unarchive release "${action.releaseId}": it is "${release.state}", and only "archived" releases can be unarchived.`
|
|
469
|
+
);
|
|
420
470
|
return store.put(bumpRev(release, { state: "active" })), { transactionId: cryptoRandomKey() };
|
|
421
471
|
}
|
|
422
472
|
case "sanity.action.release.delete": {
|
|
423
473
|
const release = getRelease(store, action.releaseId);
|
|
424
474
|
if (release.state !== "archived" && release.state !== "published")
|
|
425
|
-
throw
|
|
426
|
-
`Cannot delete release
|
|
475
|
+
throw fakeError(
|
|
476
|
+
`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.`
|
|
427
477
|
);
|
|
428
478
|
const prefix = `${VERSIONS_PREFIX$1}${action.releaseId}.`;
|
|
429
479
|
for (const v of store.all())
|
|
@@ -433,7 +483,9 @@ function applyAction(store, action) {
|
|
|
433
483
|
case "sanity.action.document.version.create": {
|
|
434
484
|
const releaseId = action.releaseId ?? (action.document._id !== void 0 ? parseVersionId(action.document._id).releaseId : void 0);
|
|
435
485
|
if (releaseId === void 0)
|
|
436
|
-
throw
|
|
486
|
+
throw fakeError(
|
|
487
|
+
'version.create needs a release to attach to: pass `releaseId`, or a `document._id` shaped "versions.<releaseId>.<publishedId>" so the release can be inferred.'
|
|
488
|
+
);
|
|
437
489
|
const release = getRelease(store, releaseId);
|
|
438
490
|
requireActive(release);
|
|
439
491
|
const vid = versionDocId(releaseId, action.publishedId), doc = {
|
|
@@ -445,7 +497,9 @@ function applyAction(store, action) {
|
|
|
445
497
|
case "sanity.action.document.version.replace": {
|
|
446
498
|
const id = action.document._id;
|
|
447
499
|
if (typeof id != "string")
|
|
448
|
-
throw
|
|
500
|
+
throw fakeError(
|
|
501
|
+
'version.replace needs `document._id` (a version id like "versions.<releaseId>.<publishedId>") to identify which version document to replace.'
|
|
502
|
+
);
|
|
449
503
|
const { releaseId } = parseVersionId(id), release = getRelease(store, releaseId);
|
|
450
504
|
requireActive(release);
|
|
451
505
|
const existing = store.get(id), promoted = promote(action.document);
|
|
@@ -619,12 +673,193 @@ function createTransaction(executor) {
|
|
|
619
673
|
};
|
|
620
674
|
return handle;
|
|
621
675
|
}
|
|
622
|
-
const
|
|
676
|
+
const ISSUES_URL = "https://github.com/sanity-labs/client-fake-for-test/issues", SUPPORTED_SURFACE = [
|
|
677
|
+
"fetch",
|
|
678
|
+
"getDocument",
|
|
679
|
+
"getDocuments",
|
|
680
|
+
"create",
|
|
681
|
+
"createOrReplace",
|
|
682
|
+
"createIfNotExists",
|
|
683
|
+
"delete",
|
|
684
|
+
"patch",
|
|
685
|
+
"transaction",
|
|
686
|
+
"mutate",
|
|
687
|
+
"action",
|
|
688
|
+
"releases",
|
|
689
|
+
"createVersion",
|
|
690
|
+
"unpublishVersion",
|
|
691
|
+
"request (only with accessControl)",
|
|
692
|
+
"config",
|
|
693
|
+
"withConfig"
|
|
694
|
+
], UNSUPPORTED_MEMBERS = {
|
|
695
|
+
listen: {
|
|
696
|
+
what: "real-time listener (returns an RxJS Observable of mutation events)",
|
|
697
|
+
workaround: 'Test your subscription logic in isolation, or register a fake: client.stub("listen", () => myFakeObservable).'
|
|
698
|
+
},
|
|
699
|
+
observable: {
|
|
700
|
+
what: "the RxJS-flavoured mirror of the client API",
|
|
701
|
+
workaround: "Use the Promise-based methods and wrap them with `from()` if you need an Observable."
|
|
702
|
+
},
|
|
703
|
+
clone: {
|
|
704
|
+
what: "clone the client with the same config",
|
|
705
|
+
workaround: "Use `withConfig({})` \u2014 it returns a sibling handle into the same store."
|
|
706
|
+
},
|
|
707
|
+
getUrl: {
|
|
708
|
+
what: "build an absolute API URL for a path",
|
|
709
|
+
workaround: "Hard-code the expected URL in your test, or stub it."
|
|
710
|
+
},
|
|
711
|
+
getDataUrl: {
|
|
712
|
+
what: "build an absolute data-API URL",
|
|
713
|
+
workaround: "Hard-code the expected URL in your test, or stub it."
|
|
714
|
+
},
|
|
715
|
+
assets: {
|
|
716
|
+
what: "asset upload / metadata namespace",
|
|
717
|
+
workaround: "Seed asset documents into the store directly with `seed()` / `add()`."
|
|
718
|
+
},
|
|
719
|
+
agent: {
|
|
720
|
+
what: "Agent Actions namespace (generate / transform / \u2026)",
|
|
721
|
+
workaround: 'Stub the call site: client.stub("agent", myFakeAgent).'
|
|
722
|
+
},
|
|
723
|
+
projects: {
|
|
724
|
+
what: "project-management namespace",
|
|
725
|
+
workaround: "Out of scope for a data-plane fake \u2014 stub the call site."
|
|
726
|
+
},
|
|
727
|
+
users: {
|
|
728
|
+
what: "user-management namespace",
|
|
729
|
+
workaround: "Out of scope for a data-plane fake \u2014 stub the call site."
|
|
730
|
+
},
|
|
731
|
+
live: {
|
|
732
|
+
what: "Live Content API namespace",
|
|
733
|
+
workaround: "Out of scope \u2014 stub the call site or test the consumer separately."
|
|
734
|
+
},
|
|
735
|
+
mediaLibrary: {
|
|
736
|
+
what: "Media Library namespace",
|
|
737
|
+
workaround: "Out of scope \u2014 seed documents directly or stub the call site."
|
|
738
|
+
},
|
|
739
|
+
request: {
|
|
740
|
+
what: "generic HTTP request hook \u2014 only wired up when accessControl is configured",
|
|
741
|
+
workaround: "Create the client with `accessControl` to enable request() (it serves /users/me + the ACL path), or register a stub."
|
|
742
|
+
},
|
|
743
|
+
dryRun: {
|
|
744
|
+
what: "not a method \u2014 did you mean the `{ dryRun }` mutation option?",
|
|
745
|
+
workaround: "Pass `{ dryRun: true }` as the second arg to create / mutate / delete."
|
|
746
|
+
}
|
|
747
|
+
}, PROBE_NAMES = /* @__PURE__ */ new Set([
|
|
748
|
+
"then",
|
|
749
|
+
"catch",
|
|
750
|
+
"finally",
|
|
751
|
+
"toJSON",
|
|
752
|
+
"asymmetricMatch",
|
|
753
|
+
"$$typeof",
|
|
754
|
+
"nodeType",
|
|
755
|
+
"_isMockFunction"
|
|
756
|
+
]);
|
|
757
|
+
function guardMessage(path, detail, workaround) {
|
|
758
|
+
return [
|
|
759
|
+
`[client-fake-for-test] \`client.${path}\` ${detail}`,
|
|
760
|
+
"",
|
|
761
|
+
"This is an in-memory fake of @sanity/client. It implements:",
|
|
762
|
+
` ${SUPPORTED_SURFACE.join(", ")}.`,
|
|
763
|
+
"",
|
|
764
|
+
`\u2192 ${workaround}`,
|
|
765
|
+
"",
|
|
766
|
+
`Register a stub to fill any gap: client.stub(${JSON.stringify(path.split(".")[0])}, handler)`,
|
|
767
|
+
`If this should be supported, open an issue: ${ISSUES_URL}`
|
|
768
|
+
].join(`
|
|
769
|
+
`);
|
|
770
|
+
}
|
|
771
|
+
function makeGuard(path, message) {
|
|
772
|
+
const thrower = () => {
|
|
773
|
+
throw new Error(message);
|
|
774
|
+
};
|
|
775
|
+
return new Proxy(thrower, {
|
|
776
|
+
apply: () => thrower(),
|
|
777
|
+
get: (_t, prop) => {
|
|
778
|
+
if (typeof prop == "symbol" || PROBE_NAMES.has(prop)) return;
|
|
779
|
+
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.";
|
|
780
|
+
return makeGuard(childPath, guardMessage(childPath, detail, workaround));
|
|
781
|
+
}
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
function wrapRecorded(method, fn, onCall) {
|
|
785
|
+
return (...args) => {
|
|
786
|
+
const settle = onCall(method, args);
|
|
787
|
+
try {
|
|
788
|
+
const out = fn(...args);
|
|
789
|
+
return out !== null && typeof out == "object" && typeof out.then == "function" ? out.then(
|
|
790
|
+
(result) => (settle({ result }), result),
|
|
791
|
+
(error) => {
|
|
792
|
+
throw settle({ error }), error;
|
|
793
|
+
}
|
|
794
|
+
) : (settle({ result: out }), out);
|
|
795
|
+
} catch (error) {
|
|
796
|
+
throw settle({ error }), error;
|
|
797
|
+
}
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
function wrapNamespace(ns, prefix, onCall) {
|
|
801
|
+
return new Proxy(ns, {
|
|
802
|
+
get(target, prop, receiver) {
|
|
803
|
+
const value = Reflect.get(target, prop, receiver);
|
|
804
|
+
return typeof prop == "string" && typeof value == "function" ? wrapRecorded(`${prefix}.${prop}`, value, onCall) : value;
|
|
805
|
+
}
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
function wrapWithGuards(client, config) {
|
|
809
|
+
const { getStub, onCall, recorded } = config;
|
|
810
|
+
return new Proxy(client, {
|
|
811
|
+
get(target, prop, receiver) {
|
|
812
|
+
if (typeof prop == "symbol") return Reflect.get(target, prop, receiver);
|
|
813
|
+
if (prop in target) {
|
|
814
|
+
const value = Reflect.get(target, prop, receiver);
|
|
815
|
+
if (onCall !== void 0 && recorded?.has(prop) === !0) {
|
|
816
|
+
if (typeof value == "function")
|
|
817
|
+
return wrapRecorded(prop, value, onCall);
|
|
818
|
+
if (value !== null && typeof value == "object")
|
|
819
|
+
return wrapNamespace(value, prop, onCall);
|
|
820
|
+
}
|
|
821
|
+
return value;
|
|
822
|
+
}
|
|
823
|
+
if (PROBE_NAMES.has(prop)) return;
|
|
824
|
+
const stub = getStub(prop);
|
|
825
|
+
if (stub !== void 0) return stub;
|
|
826
|
+
const entry = UNSUPPORTED_MEMBERS[prop];
|
|
827
|
+
return entry !== void 0 ? makeGuard(
|
|
828
|
+
prop,
|
|
829
|
+
guardMessage(prop, `is not implemented (${entry.what}).`, entry.workaround)
|
|
830
|
+
) : makeGuard(
|
|
831
|
+
prop,
|
|
832
|
+
guardMessage(
|
|
833
|
+
prop,
|
|
834
|
+
"is not a method on this fake (unknown member or unimplemented @sanity/client API).",
|
|
835
|
+
"Check for a typo, register a stub, or use a different test strategy."
|
|
836
|
+
)
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
const RECORDED_METHODS = /* @__PURE__ */ new Set([
|
|
842
|
+
"fetch",
|
|
843
|
+
"getDocument",
|
|
844
|
+
"getDocuments",
|
|
845
|
+
"create",
|
|
846
|
+
"createOrReplace",
|
|
847
|
+
"createIfNotExists",
|
|
848
|
+
"delete",
|
|
849
|
+
"patch",
|
|
850
|
+
"transaction",
|
|
851
|
+
"mutate",
|
|
852
|
+
"action",
|
|
853
|
+
"releases",
|
|
854
|
+
"createVersion",
|
|
855
|
+
"unpublishVersion",
|
|
856
|
+
"request"
|
|
857
|
+
]), DEFAULT_PROJECT_ID = "test", DEFAULT_DATASET = "test", registryKey = (projectId, dataset) => `${projectId}:${dataset}`, parseRegistryKey = (key) => {
|
|
623
858
|
const [projectId, dataset] = key.split(":");
|
|
624
859
|
return { projectId, dataset };
|
|
625
860
|
};
|
|
626
861
|
function createTestClient(options = {}) {
|
|
627
|
-
const defaultProjectId = options.projectId ?? DEFAULT_PROJECT_ID, defaultDataset = options.dataset ?? DEFAULT_DATASET,
|
|
862
|
+
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) => {
|
|
628
863
|
const key = registryKey(projectId, dataset);
|
|
629
864
|
let store = registry.get(key);
|
|
630
865
|
return store === void 0 && (store = createTestStore(), registry.set(key, store)), store;
|
|
@@ -635,24 +870,87 @@ function createTestClient(options = {}) {
|
|
|
635
870
|
ensureStore(projectId, dataset).seed(docs);
|
|
636
871
|
}
|
|
637
872
|
return makeHandle(
|
|
638
|
-
|
|
873
|
+
backend,
|
|
639
874
|
defaultProjectId,
|
|
640
875
|
defaultDataset,
|
|
641
876
|
options.accessControl,
|
|
642
|
-
options.perspective ?? "raw"
|
|
877
|
+
options.perspective ?? "raw",
|
|
878
|
+
options.mutationGuard
|
|
643
879
|
);
|
|
644
880
|
}
|
|
645
|
-
function
|
|
646
|
-
const
|
|
881
|
+
function dedupeIds(results) {
|
|
882
|
+
const seen = /* @__PURE__ */ new Set(), out = [];
|
|
883
|
+
for (const r of results)
|
|
884
|
+
seen.has(r.id) || (seen.add(r.id), out.push(r.id));
|
|
885
|
+
return out;
|
|
886
|
+
}
|
|
887
|
+
function formatMutationResult(documents, results, transactionId, returnDocuments, returnFirst) {
|
|
888
|
+
return returnDocuments ? returnFirst ? documents[0] : documents : returnFirst ? {
|
|
889
|
+
transactionId,
|
|
890
|
+
documentId: results[0]?.id,
|
|
891
|
+
results
|
|
892
|
+
} : {
|
|
893
|
+
transactionId,
|
|
894
|
+
documentIds: dedupeIds(results),
|
|
895
|
+
results
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
function toSingleTxnMutation(op) {
|
|
899
|
+
if ("create" in op) return { kind: "create", doc: op.create };
|
|
900
|
+
if ("createOrReplace" in op) return { kind: "createOrReplace", doc: op.createOrReplace };
|
|
901
|
+
if ("createIfNotExists" in op) return { kind: "createIfNotExists", doc: op.createIfNotExists };
|
|
902
|
+
if ("delete" in op) return { kind: "delete", id: op.delete.id };
|
|
903
|
+
const p = op.patch, ops = [];
|
|
904
|
+
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) {
|
|
905
|
+
const { before, after, replace, items } = p.insert, position = before !== void 0 ? "before" : after !== void 0 ? "after" : "replace", anchor = before ?? after ?? replace;
|
|
906
|
+
if (anchor === void 0)
|
|
907
|
+
throw fakeError(
|
|
908
|
+
'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.'
|
|
909
|
+
);
|
|
910
|
+
ops.push({ kind: "insert", position, anchor, items });
|
|
911
|
+
}
|
|
912
|
+
return {
|
|
913
|
+
kind: "patch",
|
|
914
|
+
documentId: p.id,
|
|
915
|
+
ops,
|
|
916
|
+
...p.ifRevisionID !== void 0 ? { ifRev: p.ifRevisionID } : {}
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
function toTxnMutations(operations) {
|
|
920
|
+
if (Array.isArray(operations)) return operations.map(toSingleTxnMutation);
|
|
921
|
+
const json = operations.toJSON();
|
|
922
|
+
return Array.isArray(json) ? json : [
|
|
923
|
+
{
|
|
924
|
+
kind: "patch",
|
|
925
|
+
documentId: json.id,
|
|
926
|
+
ops: json.ops,
|
|
927
|
+
...json.ifRev !== void 0 ? { ifRev: json.ifRev } : {}
|
|
928
|
+
}
|
|
929
|
+
];
|
|
930
|
+
}
|
|
931
|
+
function makeHandle(backend, projectId, dataset, accessControl, perspective, mutationGuard) {
|
|
932
|
+
const { registry } = backend, store = (() => {
|
|
647
933
|
const key = registryKey(projectId, dataset);
|
|
648
934
|
let s = registry.get(key);
|
|
649
935
|
return s === void 0 && (s = createTestStore(), registry.set(key, s)), s;
|
|
650
|
-
})(),
|
|
936
|
+
})(), record = (transactionId, mutations) => {
|
|
937
|
+
backend.log.push({ transactionId, projectId, dataset, mutations });
|
|
938
|
+
}, writeResult = (documents, results, transactionId, opts) => formatMutationResult(
|
|
939
|
+
documents,
|
|
940
|
+
results,
|
|
941
|
+
transactionId,
|
|
942
|
+
opts?.returnDocuments ?? !0,
|
|
943
|
+
opts?.returnFirst ?? !0
|
|
944
|
+
), promote2 = (doc) => {
|
|
651
945
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
652
946
|
if (typeof doc._id != "string" || doc._id.length === 0)
|
|
653
|
-
throw
|
|
947
|
+
throw fakeError(
|
|
948
|
+
`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.`
|
|
949
|
+
);
|
|
654
950
|
if (typeof doc._type != "string" || doc._type.length === 0)
|
|
655
|
-
throw
|
|
951
|
+
throw fakeError(
|
|
952
|
+
`Document "${doc._id}" is missing a non-empty string \`_type\` (got ${JSON.stringify(doc._type)}). Every Sanity document needs a _type.`
|
|
953
|
+
);
|
|
656
954
|
return {
|
|
657
955
|
...doc,
|
|
658
956
|
_id: doc._id,
|
|
@@ -665,10 +963,12 @@ function makeHandle(registry, projectId, dataset, accessControl, perspective) {
|
|
|
665
963
|
if (ifRevisionId === void 0) return;
|
|
666
964
|
const existing = store.get(id);
|
|
667
965
|
if (existing === void 0)
|
|
668
|
-
throw
|
|
966
|
+
throw fakeError(
|
|
967
|
+
`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.`
|
|
968
|
+
);
|
|
669
969
|
if (existing._rev !== ifRevisionId)
|
|
670
|
-
throw
|
|
671
|
-
`ifRevisionId check failed: document ${id}
|
|
970
|
+
throw fakeError(
|
|
971
|
+
`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.)`
|
|
672
972
|
);
|
|
673
973
|
}, gate = async (document, permission) => {
|
|
674
974
|
accessControl !== void 0 && await enforceWriteAccess({
|
|
@@ -677,6 +977,8 @@ function makeHandle(registry, projectId, dataset, accessControl, perspective) {
|
|
|
677
977
|
permission,
|
|
678
978
|
userId: accessControl.currentUser.id
|
|
679
979
|
});
|
|
980
|
+
}, guardGate = async (action, before, after) => {
|
|
981
|
+
mutationGuard !== void 0 && await mutationGuard({ action, before, after, identity: accessControl?.currentUser.id });
|
|
680
982
|
}, canRead = async (document) => accessControl === void 0 ? !0 : canAccess({
|
|
681
983
|
document,
|
|
682
984
|
grants: accessControl.grants,
|
|
@@ -685,16 +987,18 @@ function makeHandle(registry, projectId, dataset, accessControl, perspective) {
|
|
|
685
987
|
}), applyPatch = async (documentId, ops, ifRev) => {
|
|
686
988
|
const existing = store.get(documentId);
|
|
687
989
|
if (existing === void 0)
|
|
688
|
-
throw
|
|
990
|
+
throw fakeError(
|
|
991
|
+
`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.`
|
|
992
|
+
);
|
|
689
993
|
checkRevision(documentId, ifRev), await gate(existing, "update");
|
|
690
994
|
const patched = applyPatchOps(existing, ops), now = (/* @__PURE__ */ new Date()).toISOString(), next = {
|
|
691
995
|
...patched,
|
|
692
996
|
_rev: cryptoRandomKey(),
|
|
693
997
|
_updatedAt: now
|
|
694
998
|
};
|
|
695
|
-
return store.put(next), next;
|
|
999
|
+
return await guardGate("update", existing, next), store.put(next), next;
|
|
696
1000
|
}, applyTransaction = async (mutations, txnOptions) => {
|
|
697
|
-
const snapshot = /* @__PURE__ */ new Map(), touchedIds = [], operations = [], remember = (id) => {
|
|
1001
|
+
const transactionId = txnOptions?.transactionId ?? cryptoRandomKey(), snapshot = /* @__PURE__ */ new Map(), touchedIds = [], operations = [], remember = (id) => {
|
|
698
1002
|
snapshot.has(id) || snapshot.set(id, store.get(id));
|
|
699
1003
|
};
|
|
700
1004
|
try {
|
|
@@ -704,14 +1008,20 @@ function makeHandle(registry, projectId, dataset, accessControl, perspective) {
|
|
|
704
1008
|
remember(mut.doc._id);
|
|
705
1009
|
const promoted = promote2(mut.doc);
|
|
706
1010
|
if (store.get(promoted._id) !== void 0)
|
|
707
|
-
throw
|
|
708
|
-
|
|
1011
|
+
throw fakeError(
|
|
1012
|
+
`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.`
|
|
1013
|
+
);
|
|
1014
|
+
await gate(promoted, "create"), await guardGate("create", null, promoted), store.put(promoted), touchedIds.push(promoted._id), operations.push({ id: promoted._id, operation: "create" });
|
|
709
1015
|
break;
|
|
710
1016
|
}
|
|
711
1017
|
case "createOrReplace": {
|
|
712
1018
|
remember(mut.doc._id);
|
|
713
1019
|
const promoted = promote2(mut.doc), existing = store.get(promoted._id);
|
|
714
|
-
await gate(existing ?? promoted, existing !== void 0 ? "update" : "create")
|
|
1020
|
+
await gate(existing ?? promoted, existing !== void 0 ? "update" : "create"), await guardGate(
|
|
1021
|
+
existing !== void 0 ? "update" : "create",
|
|
1022
|
+
existing ?? null,
|
|
1023
|
+
promoted
|
|
1024
|
+
);
|
|
715
1025
|
const wasPresent = existing !== void 0;
|
|
716
1026
|
store.put(promoted), touchedIds.push(promoted._id), operations.push({ id: promoted._id, operation: wasPresent ? "update" : "create" });
|
|
717
1027
|
break;
|
|
@@ -719,14 +1029,14 @@ function makeHandle(registry, projectId, dataset, accessControl, perspective) {
|
|
|
719
1029
|
case "createIfNotExists": {
|
|
720
1030
|
if (remember(mut.doc._id), store.get(mut.doc._id) === void 0) {
|
|
721
1031
|
const promoted = promote2(mut.doc);
|
|
722
|
-
await gate(promoted, "create"), store.put(promoted), touchedIds.push(promoted._id), operations.push({ id: promoted._id, operation: "create" });
|
|
1032
|
+
await gate(promoted, "create"), await guardGate("create", null, promoted), store.put(promoted), touchedIds.push(promoted._id), operations.push({ id: promoted._id, operation: "create" });
|
|
723
1033
|
}
|
|
724
1034
|
break;
|
|
725
1035
|
}
|
|
726
1036
|
case "delete": {
|
|
727
1037
|
remember(mut.id);
|
|
728
1038
|
const existing = store.get(mut.id);
|
|
729
|
-
existing !== void 0 && await gate(existing, "update"), store.remove(mut.id) && (touchedIds.push(mut.id), operations.push({ id: mut.id, operation: "delete" }));
|
|
1039
|
+
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" }));
|
|
730
1040
|
break;
|
|
731
1041
|
}
|
|
732
1042
|
case "patch": {
|
|
@@ -739,14 +1049,15 @@ function makeHandle(registry, projectId, dataset, accessControl, perspective) {
|
|
|
739
1049
|
doc === void 0 ? store.remove(id) : store.put(doc);
|
|
740
1050
|
throw error;
|
|
741
1051
|
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
transactionId
|
|
748
|
-
|
|
749
|
-
|
|
1052
|
+
record(transactionId, mutations);
|
|
1053
|
+
const documents = touchedIds.map((id) => store.get(id)).filter((d) => d !== void 0);
|
|
1054
|
+
return formatMutationResult(
|
|
1055
|
+
documents,
|
|
1056
|
+
operations,
|
|
1057
|
+
transactionId,
|
|
1058
|
+
txnOptions?.returnDocuments ?? !1,
|
|
1059
|
+
txnOptions?.returnFirst ?? !1
|
|
1060
|
+
);
|
|
750
1061
|
}, accessVisibleDocs = async () => {
|
|
751
1062
|
const all = store.all();
|
|
752
1063
|
if (accessControl === void 0) return all;
|
|
@@ -757,23 +1068,26 @@ function makeHandle(registry, projectId, dataset, accessControl, perspective) {
|
|
|
757
1068
|
}, visibleDocs = async (override) => {
|
|
758
1069
|
const accessible = await accessVisibleDocs();
|
|
759
1070
|
return applyPerspective(accessible, override ?? perspective);
|
|
760
|
-
}
|
|
761
|
-
return {
|
|
1071
|
+
}, client = {
|
|
762
1072
|
fetch: async (query, params, opts) => {
|
|
763
|
-
const tree = parse(query, params ? { params } : void 0)
|
|
764
|
-
return await (await evaluate(tree, {
|
|
1073
|
+
const start = Date.now(), tree = parse(query, params ? { params } : void 0), result = await (await evaluate(tree, {
|
|
765
1074
|
dataset: await visibleDocs(opts?.perspective),
|
|
766
1075
|
...params !== void 0 ? { params } : {}
|
|
767
1076
|
})).get();
|
|
1077
|
+
if (opts?.filterResponse === !1) {
|
|
1078
|
+
const ms = Date.now() - start;
|
|
1079
|
+
return opts.returnQuery === !1 ? { ms, result } : { query, ms, result };
|
|
1080
|
+
}
|
|
1081
|
+
return result;
|
|
768
1082
|
},
|
|
769
1083
|
getDocument: async (id, opts) => {
|
|
770
1084
|
const effective = opts?.perspective ?? perspective;
|
|
771
1085
|
if (effective === "raw") {
|
|
772
1086
|
const doc = store.get(id);
|
|
773
|
-
return doc === void 0 || !await canRead(doc) ?
|
|
1087
|
+
return doc === void 0 || !await canRead(doc) ? void 0 : doc;
|
|
774
1088
|
}
|
|
775
1089
|
const view = await visibleDocs(effective);
|
|
776
|
-
return resolveDocument(view, id, effective) ??
|
|
1090
|
+
return resolveDocument(view, id, effective) ?? void 0;
|
|
777
1091
|
},
|
|
778
1092
|
getDocuments: async (ids, opts) => {
|
|
779
1093
|
const effective = opts?.perspective ?? perspective;
|
|
@@ -790,29 +1104,75 @@ function makeHandle(registry, projectId, dataset, accessControl, perspective) {
|
|
|
790
1104
|
},
|
|
791
1105
|
create: async (doc, opts) => {
|
|
792
1106
|
if (checkRevision(doc._id, opts?.ifRevisionId), store.get(doc._id) !== void 0)
|
|
793
|
-
throw
|
|
1107
|
+
throw fakeError(
|
|
1108
|
+
`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.`
|
|
1109
|
+
);
|
|
794
1110
|
const promoted = promote2(doc);
|
|
795
|
-
|
|
1111
|
+
await gate(promoted, "create"), await guardGate("create", null, promoted);
|
|
1112
|
+
const transactionId = opts?.transactionId ?? cryptoRandomKey();
|
|
1113
|
+
return opts?.dryRun !== !0 && (store.put(promoted), record(transactionId, [{ kind: "create", doc }])), writeResult(
|
|
1114
|
+
[promoted],
|
|
1115
|
+
[{ id: promoted._id, operation: "create" }],
|
|
1116
|
+
transactionId,
|
|
1117
|
+
opts
|
|
1118
|
+
);
|
|
796
1119
|
},
|
|
797
1120
|
createOrReplace: async (doc, opts) => {
|
|
798
1121
|
checkRevision(doc._id, opts?.ifRevisionId);
|
|
799
1122
|
const promoted = promote2(doc), existing = store.get(doc._id);
|
|
800
|
-
|
|
1123
|
+
await gate(existing ?? promoted, existing !== void 0 ? "update" : "create"), await guardGate(existing !== void 0 ? "update" : "create", existing ?? null, promoted);
|
|
1124
|
+
const transactionId = opts?.transactionId ?? cryptoRandomKey();
|
|
1125
|
+
opts?.dryRun !== !0 && (store.put(promoted), record(transactionId, [{ kind: "createOrReplace", doc }]));
|
|
1126
|
+
const operation = existing !== void 0 ? "update" : "create";
|
|
1127
|
+
return writeResult(
|
|
1128
|
+
[promoted],
|
|
1129
|
+
[{ id: promoted._id, operation }],
|
|
1130
|
+
transactionId,
|
|
1131
|
+
opts
|
|
1132
|
+
);
|
|
801
1133
|
},
|
|
802
1134
|
createIfNotExists: async (doc, opts) => {
|
|
803
1135
|
checkRevision(doc._id, opts?.ifRevisionId);
|
|
804
|
-
const existing = store.get(doc._id);
|
|
805
|
-
if (existing !== void 0)
|
|
1136
|
+
const existing = store.get(doc._id), transactionId = opts?.transactionId ?? cryptoRandomKey();
|
|
1137
|
+
if (existing !== void 0)
|
|
1138
|
+
return writeResult(
|
|
1139
|
+
[existing],
|
|
1140
|
+
[{ id: existing._id, operation: "none" }],
|
|
1141
|
+
transactionId,
|
|
1142
|
+
opts
|
|
1143
|
+
);
|
|
806
1144
|
const promoted = promote2(doc);
|
|
807
|
-
return await gate(promoted, "create"), store.put(promoted),
|
|
1145
|
+
return await gate(promoted, "create"), await guardGate("create", null, promoted), opts?.dryRun !== !0 && (store.put(promoted), record(transactionId, [{ kind: "createIfNotExists", doc }])), writeResult(
|
|
1146
|
+
[promoted],
|
|
1147
|
+
[{ id: promoted._id, operation: "create" }],
|
|
1148
|
+
transactionId,
|
|
1149
|
+
opts
|
|
1150
|
+
);
|
|
808
1151
|
},
|
|
809
1152
|
delete: async (id, opts) => {
|
|
810
1153
|
checkRevision(id, opts?.ifRevisionId);
|
|
811
1154
|
const existing = store.get(id);
|
|
812
|
-
|
|
1155
|
+
existing !== void 0 && await gate(existing, "update"), existing !== void 0 && await guardGate("delete", existing, null);
|
|
1156
|
+
const transactionId = opts?.transactionId ?? cryptoRandomKey();
|
|
1157
|
+
let removed = existing;
|
|
1158
|
+
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(
|
|
1159
|
+
removed !== void 0 ? [removed] : [],
|
|
1160
|
+
removed !== void 0 ? [{ id, operation: "delete" }] : [],
|
|
1161
|
+
transactionId,
|
|
1162
|
+
opts.returnDocuments ?? !0,
|
|
1163
|
+
opts.returnFirst ?? !0
|
|
1164
|
+
);
|
|
813
1165
|
},
|
|
814
|
-
patch: (documentId) => createPatch(documentId, {
|
|
1166
|
+
patch: (documentId) => createPatch(documentId, {
|
|
1167
|
+
apply: async (docId, ops, ifRev) => {
|
|
1168
|
+
const doc = await applyPatch(docId, ops, ifRev);
|
|
1169
|
+
return record(cryptoRandomKey(), [
|
|
1170
|
+
{ kind: "patch", documentId: docId, ops, ...ifRev !== void 0 ? { ifRev } : {} }
|
|
1171
|
+
]), doc;
|
|
1172
|
+
}
|
|
1173
|
+
}),
|
|
815
1174
|
transaction: () => createTransaction({ apply: applyTransaction }),
|
|
1175
|
+
mutate: async (operations, options) => applyTransaction(toTxnMutations(operations), options),
|
|
816
1176
|
action: async (action) => applyActions(store, action),
|
|
817
1177
|
releases: {
|
|
818
1178
|
create: async ({ releaseId, metadata }) => {
|
|
@@ -858,21 +1218,49 @@ function makeHandle(registry, projectId, dataset, accessControl, perspective) {
|
|
|
858
1218
|
...accessControl !== void 0 ? { request: makeRequestImpl(accessControl, projectId, dataset) } : {},
|
|
859
1219
|
config: () => ({ projectId, dataset, perspective }),
|
|
860
1220
|
withConfig: (config) => makeHandle(
|
|
861
|
-
|
|
1221
|
+
backend,
|
|
862
1222
|
config.projectId ?? projectId,
|
|
863
1223
|
config.dataset ?? dataset,
|
|
864
|
-
accessControl,
|
|
865
|
-
config.perspective ?? perspective
|
|
1224
|
+
config.accessControl ?? accessControl,
|
|
1225
|
+
config.perspective ?? perspective,
|
|
1226
|
+
mutationGuard
|
|
866
1227
|
),
|
|
867
1228
|
seed: (documents) => store.seed(documents),
|
|
868
1229
|
add: (documents) => store.add(documents),
|
|
869
1230
|
reset: () => store.reset(),
|
|
870
1231
|
snapshot: () => store.snapshot(),
|
|
871
1232
|
store: () => store,
|
|
872
|
-
datasets: () => [...registry.keys()].map(parseRegistryKey)
|
|
1233
|
+
datasets: () => [...registry.keys()].map(parseRegistryKey),
|
|
1234
|
+
stub: (name, handler) => {
|
|
1235
|
+
backend.stubs.set(name, handler);
|
|
1236
|
+
},
|
|
1237
|
+
stubs: () => Object.fromEntries(backend.stubs),
|
|
1238
|
+
mutations: () => backend.log.flatMap((entry) => entry.mutations),
|
|
1239
|
+
transactions: () => [...backend.log],
|
|
1240
|
+
clearLog: () => {
|
|
1241
|
+
backend.log.length = 0;
|
|
1242
|
+
},
|
|
1243
|
+
calls: (method) => method === void 0 ? [...backend.calls] : backend.calls.filter((c) => c.method === method),
|
|
1244
|
+
callCount: (method) => method === void 0 ? backend.calls.length : backend.calls.filter((c) => c.method === method).length,
|
|
1245
|
+
clearCalls: () => {
|
|
1246
|
+
backend.calls.length = 0;
|
|
1247
|
+
}
|
|
873
1248
|
};
|
|
1249
|
+
return wrapWithGuards(client, {
|
|
1250
|
+
getStub: (name) => backend.stubs.get(name),
|
|
1251
|
+
onCall: (method, args) => {
|
|
1252
|
+
const rec = { method, args, projectId, dataset };
|
|
1253
|
+
return backend.calls.push(rec), (outcome) => {
|
|
1254
|
+
"error" in outcome ? rec.error = outcome.error : rec.result = outcome.result;
|
|
1255
|
+
};
|
|
1256
|
+
},
|
|
1257
|
+
recorded: RECORDED_METHODS
|
|
1258
|
+
});
|
|
874
1259
|
}
|
|
875
1260
|
export {
|
|
1261
|
+
ERR_PREFIX,
|
|
1262
|
+
SUPPORTED_SURFACE,
|
|
1263
|
+
UNSUPPORTED_MEMBERS,
|
|
876
1264
|
WriteAccessDeniedError,
|
|
877
1265
|
applyPatchOps,
|
|
878
1266
|
applyPerspective,
|
|
@@ -882,11 +1270,13 @@ export {
|
|
|
882
1270
|
createTransaction,
|
|
883
1271
|
defaultAclPath,
|
|
884
1272
|
evaluateWriteAccess,
|
|
1273
|
+
fakeError,
|
|
885
1274
|
isDraftId,
|
|
886
1275
|
isReleaseDocId,
|
|
887
1276
|
isSystemId,
|
|
888
1277
|
isVersionId,
|
|
889
1278
|
publishedIdOf,
|
|
890
|
-
releaseNameOf
|
|
1279
|
+
releaseNameOf,
|
|
1280
|
+
wrapWithGuards
|
|
891
1281
|
};
|
|
892
1282
|
//# sourceMappingURL=index.js.map
|