@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.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,17 +69,21 @@ function makeRequestImpl(accessControl, projectId, dataset) {
|
|
|
65
69
|
return accessControl.currentUser;
|
|
66
70
|
if (path === aclPath)
|
|
67
71
|
return accessControl.grants;
|
|
68
|
-
throw
|
|
69
|
-
`
|
|
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
|
}
|
|
73
77
|
function createTestStore(initial) {
|
|
74
78
|
const docs = /* @__PURE__ */ new Map(), promote2 = (doc) => {
|
|
75
79
|
if (typeof doc._id != "string" || doc._id.length === 0)
|
|
76
|
-
throw
|
|
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
|
+
);
|
|
77
83
|
if (typeof doc._type != "string" || doc._type.length === 0)
|
|
78
|
-
throw
|
|
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
|
+
);
|
|
79
87
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
80
88
|
return {
|
|
81
89
|
...doc,
|
|
@@ -117,6 +125,9 @@ function cryptoRandomKey(length = 12) {
|
|
|
117
125
|
const bytes = new Uint8Array(length);
|
|
118
126
|
return globalThis.crypto.getRandomValues(bytes), [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, length);
|
|
119
127
|
}
|
|
128
|
+
function describeType(value) {
|
|
129
|
+
return value === null ? "null" : Array.isArray(value) ? "an array" : `a ${typeof value}`;
|
|
130
|
+
}
|
|
120
131
|
function createPatch(documentId, executor) {
|
|
121
132
|
const ops = [];
|
|
122
133
|
let ifRev;
|
|
@@ -177,7 +188,10 @@ function parsePath(path) {
|
|
|
177
188
|
const segments = [];
|
|
178
189
|
for (const raw of path.split(".")) {
|
|
179
190
|
const match = /^([A-Za-z_$][\w$]*)(?:\[(.+)\])?$/.exec(raw);
|
|
180
|
-
if (!match)
|
|
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
|
+
);
|
|
181
195
|
const key = match[1], selectorRaw = match[2];
|
|
182
196
|
if (selectorRaw === void 0) {
|
|
183
197
|
segments.push({ key });
|
|
@@ -196,7 +210,9 @@ function parsePath(path) {
|
|
|
196
210
|
});
|
|
197
211
|
continue;
|
|
198
212
|
}
|
|
199
|
-
throw
|
|
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
|
+
);
|
|
200
216
|
}
|
|
201
217
|
return segments;
|
|
202
218
|
}
|
|
@@ -206,7 +222,9 @@ function resolveSelector(array, selector) {
|
|
|
206
222
|
(entry) => typeof entry == "object" && entry !== null && entry[selector.field] === selector.value
|
|
207
223
|
);
|
|
208
224
|
if (idx === -1)
|
|
209
|
-
throw
|
|
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
|
+
);
|
|
210
228
|
return idx;
|
|
211
229
|
}
|
|
212
230
|
function getAtPath(doc, path) {
|
|
@@ -224,14 +242,17 @@ function getAtPath(doc, path) {
|
|
|
224
242
|
}
|
|
225
243
|
function setAtPath(doc, path, value) {
|
|
226
244
|
const segments = parsePath(path);
|
|
227
|
-
if (segments.length === 0)
|
|
245
|
+
if (segments.length === 0)
|
|
246
|
+
throw fakeError("Empty patch path \u2014 a set/setIfMissing op needs a target field.");
|
|
228
247
|
let cursor = doc;
|
|
229
248
|
for (let i = 0; i < segments.length - 1; i++) {
|
|
230
249
|
const seg = segments[i];
|
|
231
250
|
let next = cursor[seg.key];
|
|
232
251
|
if (next === void 0 && (next = seg.selector?.kind === "index" || seg.selector?.kind === "keyed" ? [] : {}, cursor[seg.key] = next), seg.selector !== void 0) {
|
|
233
252
|
if (!Array.isArray(next))
|
|
234
|
-
throw
|
|
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
|
+
);
|
|
235
256
|
const idx = resolveSelector(next, seg.selector);
|
|
236
257
|
cursor = next[idx];
|
|
237
258
|
} else
|
|
@@ -241,7 +262,9 @@ function setAtPath(doc, path, value) {
|
|
|
241
262
|
if (last.selector !== void 0) {
|
|
242
263
|
const arr = cursor[last.key];
|
|
243
264
|
if (!Array.isArray(arr))
|
|
244
|
-
throw
|
|
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
|
+
);
|
|
245
268
|
const idx = resolveSelector(arr, last.selector);
|
|
246
269
|
arr[idx] = value;
|
|
247
270
|
} else
|
|
@@ -273,41 +296,56 @@ function unsetAtPath(doc, path) {
|
|
|
273
296
|
}
|
|
274
297
|
function insertIntoArray(doc, position, anchor, items) {
|
|
275
298
|
const segments = parsePath(anchor);
|
|
276
|
-
if (segments.length === 0)
|
|
299
|
+
if (segments.length === 0)
|
|
300
|
+
throw fakeError("Empty insert anchor \u2014 insert() needs an array path to anchor against.");
|
|
277
301
|
const last = segments[segments.length - 1];
|
|
278
302
|
if (last.selector === void 0)
|
|
279
|
-
throw
|
|
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
|
+
);
|
|
280
306
|
let cursor = doc;
|
|
281
307
|
for (let i = 0; i < segments.length - 1; i++) {
|
|
282
308
|
const seg = segments[i], next = cursor[seg.key];
|
|
283
309
|
if (next == null)
|
|
284
|
-
throw
|
|
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
|
+
);
|
|
285
313
|
if (seg.selector !== void 0) {
|
|
286
314
|
if (!Array.isArray(next))
|
|
287
|
-
throw
|
|
315
|
+
throw fakeError(
|
|
316
|
+
`Insert anchor "${anchor}" applies an array selector to field "${seg.key}", but it holds ${describeType(next)}, not an array.`
|
|
317
|
+
);
|
|
288
318
|
cursor = next[resolveSelector(next, seg.selector)];
|
|
289
319
|
} else
|
|
290
320
|
cursor = next;
|
|
291
321
|
}
|
|
292
322
|
const arr = cursor[last.key];
|
|
293
323
|
if (!Array.isArray(arr))
|
|
294
|
-
throw
|
|
324
|
+
throw fakeError(
|
|
325
|
+
`Insert anchor "${anchor}" targets field "${last.key}", but it holds ${describeType(arr)}, not an array.`
|
|
326
|
+
);
|
|
295
327
|
const idx = resolveSelector(arr, last.selector);
|
|
296
328
|
position === "replace" ? arr.splice(idx, 1, ...items) : position === "before" ? arr.splice(idx, 0, ...items) : arr.splice(idx + 1, 0, ...items);
|
|
297
329
|
}
|
|
298
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}`;
|
|
299
331
|
function parseVersionId(versionId) {
|
|
300
332
|
if (!versionId.startsWith(VERSIONS_PREFIX$1))
|
|
301
|
-
throw
|
|
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
|
+
);
|
|
302
336
|
const rest = versionId.slice(VERSIONS_PREFIX$1.length), dot = rest.indexOf(".");
|
|
303
337
|
if (dot === -1)
|
|
304
|
-
throw
|
|
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
|
+
);
|
|
305
341
|
return { releaseId: rest.slice(0, dot), publishedId: rest.slice(dot + 1) };
|
|
306
342
|
}
|
|
307
343
|
function getRelease(store, releaseId) {
|
|
308
344
|
const doc = store.get(releaseDocId(releaseId));
|
|
309
345
|
if (doc === void 0)
|
|
310
|
-
throw
|
|
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
|
+
);
|
|
311
349
|
return doc;
|
|
312
350
|
}
|
|
313
351
|
function maybeRelease(store, releaseId) {
|
|
@@ -337,15 +375,17 @@ function bumpRev(doc, patch) {
|
|
|
337
375
|
}
|
|
338
376
|
function requireActive(release) {
|
|
339
377
|
if (release.state !== "active")
|
|
340
|
-
throw
|
|
341
|
-
`Release ${release.name} is ${release.state}
|
|
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.`
|
|
342
380
|
);
|
|
343
381
|
}
|
|
344
382
|
function applyAction(store, action) {
|
|
345
383
|
switch (action.actionType) {
|
|
346
384
|
case "sanity.action.release.create": {
|
|
347
385
|
if (maybeRelease(store, action.releaseId) !== void 0)
|
|
348
|
-
throw
|
|
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
|
+
);
|
|
349
389
|
const now = nowIso(), metadata = {
|
|
350
390
|
releaseType: "undecided",
|
|
351
391
|
...action.metadata
|
|
@@ -370,7 +410,9 @@ function applyAction(store, action) {
|
|
|
370
410
|
case "sanity.action.release.schedule": {
|
|
371
411
|
const release = getRelease(store, action.releaseId);
|
|
372
412
|
if (release.state !== "active")
|
|
373
|
-
throw
|
|
413
|
+
throw fakeError(
|
|
414
|
+
`Cannot schedule release "${action.releaseId}": it is "${release.state}", and only "active" releases can be scheduled.`
|
|
415
|
+
);
|
|
374
416
|
return store.put(
|
|
375
417
|
bumpRev(release, {
|
|
376
418
|
state: "scheduled",
|
|
@@ -381,7 +423,9 @@ function applyAction(store, action) {
|
|
|
381
423
|
case "sanity.action.release.unschedule": {
|
|
382
424
|
const release = getRelease(store, action.releaseId);
|
|
383
425
|
if (release.state !== "scheduled")
|
|
384
|
-
throw
|
|
426
|
+
throw fakeError(
|
|
427
|
+
`Cannot unschedule release "${action.releaseId}": it is "${release.state}", and only "scheduled" releases can be unscheduled.`
|
|
428
|
+
);
|
|
385
429
|
const next = { ...release, state: "active" };
|
|
386
430
|
return delete next.publishAt, store.put(bumpRev(next, {})), { transactionId: cryptoRandomKey() };
|
|
387
431
|
}
|
|
@@ -390,7 +434,9 @@ function applyAction(store, action) {
|
|
|
390
434
|
if (release.state === "published")
|
|
391
435
|
return { transactionId: cryptoRandomKey() };
|
|
392
436
|
if (release.state !== "active" && release.state !== "scheduled")
|
|
393
|
-
throw
|
|
437
|
+
throw fakeError(
|
|
438
|
+
`Cannot publish release "${action.releaseId}": it is "${release.state}", and only "active" or "scheduled" releases can be published.`
|
|
439
|
+
);
|
|
394
440
|
const prefix = `${VERSIONS_PREFIX$1}${action.releaseId}.`, sources = store.all().filter((d) => d._id.startsWith(prefix));
|
|
395
441
|
for (const v of sources) {
|
|
396
442
|
const pubId = v._id.slice(prefix.length);
|
|
@@ -412,20 +458,24 @@ function applyAction(store, action) {
|
|
|
412
458
|
case "sanity.action.release.archive": {
|
|
413
459
|
const release = getRelease(store, action.releaseId);
|
|
414
460
|
if (release.state !== "active" && release.state !== "scheduled")
|
|
415
|
-
throw
|
|
461
|
+
throw fakeError(
|
|
462
|
+
`Cannot archive release "${action.releaseId}": it is "${release.state}", and only "active" or "scheduled" releases can be archived.`
|
|
463
|
+
);
|
|
416
464
|
return store.put(bumpRev(release, { state: "archived" })), { transactionId: cryptoRandomKey() };
|
|
417
465
|
}
|
|
418
466
|
case "sanity.action.release.unarchive": {
|
|
419
467
|
const release = getRelease(store, action.releaseId);
|
|
420
468
|
if (release.state !== "archived")
|
|
421
|
-
throw
|
|
469
|
+
throw fakeError(
|
|
470
|
+
`Cannot unarchive release "${action.releaseId}": it is "${release.state}", and only "archived" releases can be unarchived.`
|
|
471
|
+
);
|
|
422
472
|
return store.put(bumpRev(release, { state: "active" })), { transactionId: cryptoRandomKey() };
|
|
423
473
|
}
|
|
424
474
|
case "sanity.action.release.delete": {
|
|
425
475
|
const release = getRelease(store, action.releaseId);
|
|
426
476
|
if (release.state !== "archived" && release.state !== "published")
|
|
427
|
-
throw
|
|
428
|
-
`Cannot delete release
|
|
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.`
|
|
429
479
|
);
|
|
430
480
|
const prefix = `${VERSIONS_PREFIX$1}${action.releaseId}.`;
|
|
431
481
|
for (const v of store.all())
|
|
@@ -435,7 +485,9 @@ function applyAction(store, action) {
|
|
|
435
485
|
case "sanity.action.document.version.create": {
|
|
436
486
|
const releaseId = action.releaseId ?? (action.document._id !== void 0 ? parseVersionId(action.document._id).releaseId : void 0);
|
|
437
487
|
if (releaseId === void 0)
|
|
438
|
-
throw
|
|
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
|
+
);
|
|
439
491
|
const release = getRelease(store, releaseId);
|
|
440
492
|
requireActive(release);
|
|
441
493
|
const vid = versionDocId(releaseId, action.publishedId), doc = {
|
|
@@ -447,7 +499,9 @@ function applyAction(store, action) {
|
|
|
447
499
|
case "sanity.action.document.version.replace": {
|
|
448
500
|
const id = action.document._id;
|
|
449
501
|
if (typeof id != "string")
|
|
450
|
-
throw
|
|
502
|
+
throw fakeError(
|
|
503
|
+
'version.replace needs `document._id` (a version id like "versions.<releaseId>.<publishedId>") to identify which version document to replace.'
|
|
504
|
+
);
|
|
451
505
|
const { releaseId } = parseVersionId(id), release = getRelease(store, releaseId);
|
|
452
506
|
requireActive(release);
|
|
453
507
|
const existing = store.get(id), promoted = promote(action.document);
|
|
@@ -621,12 +675,193 @@ function createTransaction(executor) {
|
|
|
621
675
|
};
|
|
622
676
|
return handle;
|
|
623
677
|
}
|
|
624
|
-
const
|
|
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) => {
|
|
625
860
|
const [projectId, dataset] = key.split(":");
|
|
626
861
|
return { projectId, dataset };
|
|
627
862
|
};
|
|
628
863
|
function createTestClient(options = {}) {
|
|
629
|
-
const defaultProjectId = options.projectId ?? DEFAULT_PROJECT_ID, defaultDataset = options.dataset ?? DEFAULT_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) => {
|
|
630
865
|
const key = registryKey(projectId, dataset);
|
|
631
866
|
let store = registry.get(key);
|
|
632
867
|
return store === void 0 && (store = createTestStore(), registry.set(key, store)), store;
|
|
@@ -637,24 +872,87 @@ function createTestClient(options = {}) {
|
|
|
637
872
|
ensureStore(projectId, dataset).seed(docs);
|
|
638
873
|
}
|
|
639
874
|
return makeHandle(
|
|
640
|
-
|
|
875
|
+
backend,
|
|
641
876
|
defaultProjectId,
|
|
642
877
|
defaultDataset,
|
|
643
878
|
options.accessControl,
|
|
644
|
-
options.perspective ?? "raw"
|
|
879
|
+
options.perspective ?? "raw",
|
|
880
|
+
options.mutationGuard
|
|
645
881
|
);
|
|
646
882
|
}
|
|
647
|
-
function
|
|
648
|
-
const
|
|
883
|
+
function dedupeIds(results) {
|
|
884
|
+
const seen = /* @__PURE__ */ new Set(), out = [];
|
|
885
|
+
for (const r of results)
|
|
886
|
+
seen.has(r.id) || (seen.add(r.id), out.push(r.id));
|
|
887
|
+
return out;
|
|
888
|
+
}
|
|
889
|
+
function formatMutationResult(documents, results, transactionId, returnDocuments, returnFirst) {
|
|
890
|
+
return returnDocuments ? returnFirst ? documents[0] : documents : returnFirst ? {
|
|
891
|
+
transactionId,
|
|
892
|
+
documentId: results[0]?.id,
|
|
893
|
+
results
|
|
894
|
+
} : {
|
|
895
|
+
transactionId,
|
|
896
|
+
documentIds: dedupeIds(results),
|
|
897
|
+
results
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
function toSingleTxnMutation(op) {
|
|
901
|
+
if ("create" in op) return { kind: "create", doc: op.create };
|
|
902
|
+
if ("createOrReplace" in op) return { kind: "createOrReplace", doc: op.createOrReplace };
|
|
903
|
+
if ("createIfNotExists" in op) return { kind: "createIfNotExists", doc: op.createIfNotExists };
|
|
904
|
+
if ("delete" in op) return { kind: "delete", id: op.delete.id };
|
|
905
|
+
const p = op.patch, ops = [];
|
|
906
|
+
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) {
|
|
907
|
+
const { before, after, replace, items } = p.insert, position = before !== void 0 ? "before" : after !== void 0 ? "after" : "replace", anchor = before ?? after ?? replace;
|
|
908
|
+
if (anchor === void 0)
|
|
909
|
+
throw fakeError(
|
|
910
|
+
'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.'
|
|
911
|
+
);
|
|
912
|
+
ops.push({ kind: "insert", position, anchor, items });
|
|
913
|
+
}
|
|
914
|
+
return {
|
|
915
|
+
kind: "patch",
|
|
916
|
+
documentId: p.id,
|
|
917
|
+
ops,
|
|
918
|
+
...p.ifRevisionID !== void 0 ? { ifRev: p.ifRevisionID } : {}
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
function toTxnMutations(operations) {
|
|
922
|
+
if (Array.isArray(operations)) return operations.map(toSingleTxnMutation);
|
|
923
|
+
const json = operations.toJSON();
|
|
924
|
+
return Array.isArray(json) ? json : [
|
|
925
|
+
{
|
|
926
|
+
kind: "patch",
|
|
927
|
+
documentId: json.id,
|
|
928
|
+
ops: json.ops,
|
|
929
|
+
...json.ifRev !== void 0 ? { ifRev: json.ifRev } : {}
|
|
930
|
+
}
|
|
931
|
+
];
|
|
932
|
+
}
|
|
933
|
+
function makeHandle(backend, projectId, dataset, accessControl, perspective, mutationGuard) {
|
|
934
|
+
const { registry } = backend, store = (() => {
|
|
649
935
|
const key = registryKey(projectId, dataset);
|
|
650
936
|
let s = registry.get(key);
|
|
651
937
|
return s === void 0 && (s = createTestStore(), registry.set(key, s)), s;
|
|
652
|
-
})(),
|
|
938
|
+
})(), record = (transactionId, mutations) => {
|
|
939
|
+
backend.log.push({ transactionId, projectId, dataset, mutations });
|
|
940
|
+
}, writeResult = (documents, results, transactionId, opts) => formatMutationResult(
|
|
941
|
+
documents,
|
|
942
|
+
results,
|
|
943
|
+
transactionId,
|
|
944
|
+
opts?.returnDocuments ?? !0,
|
|
945
|
+
opts?.returnFirst ?? !0
|
|
946
|
+
), promote2 = (doc) => {
|
|
653
947
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
654
948
|
if (typeof doc._id != "string" || doc._id.length === 0)
|
|
655
|
-
throw
|
|
949
|
+
throw fakeError(
|
|
950
|
+
`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.`
|
|
951
|
+
);
|
|
656
952
|
if (typeof doc._type != "string" || doc._type.length === 0)
|
|
657
|
-
throw
|
|
953
|
+
throw fakeError(
|
|
954
|
+
`Document "${doc._id}" is missing a non-empty string \`_type\` (got ${JSON.stringify(doc._type)}). Every Sanity document needs a _type.`
|
|
955
|
+
);
|
|
658
956
|
return {
|
|
659
957
|
...doc,
|
|
660
958
|
_id: doc._id,
|
|
@@ -667,10 +965,12 @@ function makeHandle(registry, projectId, dataset, accessControl, perspective) {
|
|
|
667
965
|
if (ifRevisionId === void 0) return;
|
|
668
966
|
const existing = store.get(id);
|
|
669
967
|
if (existing === void 0)
|
|
670
|
-
throw
|
|
968
|
+
throw fakeError(
|
|
969
|
+
`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.`
|
|
970
|
+
);
|
|
671
971
|
if (existing._rev !== ifRevisionId)
|
|
672
|
-
throw
|
|
673
|
-
`ifRevisionId check failed: document ${id}
|
|
972
|
+
throw fakeError(
|
|
973
|
+
`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.)`
|
|
674
974
|
);
|
|
675
975
|
}, gate = async (document, permission) => {
|
|
676
976
|
accessControl !== void 0 && await enforceWriteAccess({
|
|
@@ -679,6 +979,8 @@ function makeHandle(registry, projectId, dataset, accessControl, perspective) {
|
|
|
679
979
|
permission,
|
|
680
980
|
userId: accessControl.currentUser.id
|
|
681
981
|
});
|
|
982
|
+
}, guardGate = async (action, before, after) => {
|
|
983
|
+
mutationGuard !== void 0 && await mutationGuard({ action, before, after, identity: accessControl?.currentUser.id });
|
|
682
984
|
}, canRead = async (document) => accessControl === void 0 ? !0 : canAccess({
|
|
683
985
|
document,
|
|
684
986
|
grants: accessControl.grants,
|
|
@@ -687,16 +989,18 @@ function makeHandle(registry, projectId, dataset, accessControl, perspective) {
|
|
|
687
989
|
}), applyPatch = async (documentId, ops, ifRev) => {
|
|
688
990
|
const existing = store.get(documentId);
|
|
689
991
|
if (existing === void 0)
|
|
690
|
-
throw
|
|
992
|
+
throw fakeError(
|
|
993
|
+
`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.`
|
|
994
|
+
);
|
|
691
995
|
checkRevision(documentId, ifRev), await gate(existing, "update");
|
|
692
996
|
const patched = applyPatchOps(existing, ops), now = (/* @__PURE__ */ new Date()).toISOString(), next = {
|
|
693
997
|
...patched,
|
|
694
998
|
_rev: cryptoRandomKey(),
|
|
695
999
|
_updatedAt: now
|
|
696
1000
|
};
|
|
697
|
-
return store.put(next), next;
|
|
1001
|
+
return await guardGate("update", existing, next), store.put(next), next;
|
|
698
1002
|
}, applyTransaction = async (mutations, txnOptions) => {
|
|
699
|
-
const snapshot = /* @__PURE__ */ new Map(), touchedIds = [], operations = [], remember = (id) => {
|
|
1003
|
+
const transactionId = txnOptions?.transactionId ?? cryptoRandomKey(), snapshot = /* @__PURE__ */ new Map(), touchedIds = [], operations = [], remember = (id) => {
|
|
700
1004
|
snapshot.has(id) || snapshot.set(id, store.get(id));
|
|
701
1005
|
};
|
|
702
1006
|
try {
|
|
@@ -706,14 +1010,20 @@ function makeHandle(registry, projectId, dataset, accessControl, perspective) {
|
|
|
706
1010
|
remember(mut.doc._id);
|
|
707
1011
|
const promoted = promote2(mut.doc);
|
|
708
1012
|
if (store.get(promoted._id) !== void 0)
|
|
709
|
-
throw
|
|
710
|
-
|
|
1013
|
+
throw fakeError(
|
|
1014
|
+
`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.`
|
|
1015
|
+
);
|
|
1016
|
+
await gate(promoted, "create"), await guardGate("create", null, promoted), store.put(promoted), touchedIds.push(promoted._id), operations.push({ id: promoted._id, operation: "create" });
|
|
711
1017
|
break;
|
|
712
1018
|
}
|
|
713
1019
|
case "createOrReplace": {
|
|
714
1020
|
remember(mut.doc._id);
|
|
715
1021
|
const promoted = promote2(mut.doc), existing = store.get(promoted._id);
|
|
716
|
-
await gate(existing ?? promoted, existing !== void 0 ? "update" : "create")
|
|
1022
|
+
await gate(existing ?? promoted, existing !== void 0 ? "update" : "create"), await guardGate(
|
|
1023
|
+
existing !== void 0 ? "update" : "create",
|
|
1024
|
+
existing ?? null,
|
|
1025
|
+
promoted
|
|
1026
|
+
);
|
|
717
1027
|
const wasPresent = existing !== void 0;
|
|
718
1028
|
store.put(promoted), touchedIds.push(promoted._id), operations.push({ id: promoted._id, operation: wasPresent ? "update" : "create" });
|
|
719
1029
|
break;
|
|
@@ -721,14 +1031,14 @@ function makeHandle(registry, projectId, dataset, accessControl, perspective) {
|
|
|
721
1031
|
case "createIfNotExists": {
|
|
722
1032
|
if (remember(mut.doc._id), store.get(mut.doc._id) === void 0) {
|
|
723
1033
|
const promoted = promote2(mut.doc);
|
|
724
|
-
await gate(promoted, "create"), store.put(promoted), touchedIds.push(promoted._id), operations.push({ id: promoted._id, operation: "create" });
|
|
1034
|
+
await gate(promoted, "create"), await guardGate("create", null, promoted), store.put(promoted), touchedIds.push(promoted._id), operations.push({ id: promoted._id, operation: "create" });
|
|
725
1035
|
}
|
|
726
1036
|
break;
|
|
727
1037
|
}
|
|
728
1038
|
case "delete": {
|
|
729
1039
|
remember(mut.id);
|
|
730
1040
|
const existing = store.get(mut.id);
|
|
731
|
-
existing !== void 0 && await gate(existing, "update"), store.remove(mut.id) && (touchedIds.push(mut.id), operations.push({ id: mut.id, operation: "delete" }));
|
|
1041
|
+
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" }));
|
|
732
1042
|
break;
|
|
733
1043
|
}
|
|
734
1044
|
case "patch": {
|
|
@@ -741,14 +1051,15 @@ function makeHandle(registry, projectId, dataset, accessControl, perspective) {
|
|
|
741
1051
|
doc === void 0 ? store.remove(id) : store.put(doc);
|
|
742
1052
|
throw error;
|
|
743
1053
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
transactionId
|
|
750
|
-
|
|
751
|
-
|
|
1054
|
+
record(transactionId, mutations);
|
|
1055
|
+
const documents = touchedIds.map((id) => store.get(id)).filter((d) => d !== void 0);
|
|
1056
|
+
return formatMutationResult(
|
|
1057
|
+
documents,
|
|
1058
|
+
operations,
|
|
1059
|
+
transactionId,
|
|
1060
|
+
txnOptions?.returnDocuments ?? !1,
|
|
1061
|
+
txnOptions?.returnFirst ?? !1
|
|
1062
|
+
);
|
|
752
1063
|
}, accessVisibleDocs = async () => {
|
|
753
1064
|
const all = store.all();
|
|
754
1065
|
if (accessControl === void 0) return all;
|
|
@@ -759,23 +1070,26 @@ function makeHandle(registry, projectId, dataset, accessControl, perspective) {
|
|
|
759
1070
|
}, visibleDocs = async (override) => {
|
|
760
1071
|
const accessible = await accessVisibleDocs();
|
|
761
1072
|
return applyPerspective(accessible, override ?? perspective);
|
|
762
|
-
}
|
|
763
|
-
return {
|
|
1073
|
+
}, client = {
|
|
764
1074
|
fetch: async (query, params, opts) => {
|
|
765
|
-
const tree = groqJs.parse(query, params ? { params } : void 0)
|
|
766
|
-
return await (await groqJs.evaluate(tree, {
|
|
1075
|
+
const start = Date.now(), tree = groqJs.parse(query, params ? { params } : void 0), result = await (await groqJs.evaluate(tree, {
|
|
767
1076
|
dataset: await visibleDocs(opts?.perspective),
|
|
768
1077
|
...params !== void 0 ? { params } : {}
|
|
769
1078
|
})).get();
|
|
1079
|
+
if (opts?.filterResponse === !1) {
|
|
1080
|
+
const ms = Date.now() - start;
|
|
1081
|
+
return opts.returnQuery === !1 ? { ms, result } : { query, ms, result };
|
|
1082
|
+
}
|
|
1083
|
+
return result;
|
|
770
1084
|
},
|
|
771
1085
|
getDocument: async (id, opts) => {
|
|
772
1086
|
const effective = opts?.perspective ?? perspective;
|
|
773
1087
|
if (effective === "raw") {
|
|
774
1088
|
const doc = store.get(id);
|
|
775
|
-
return doc === void 0 || !await canRead(doc) ?
|
|
1089
|
+
return doc === void 0 || !await canRead(doc) ? void 0 : doc;
|
|
776
1090
|
}
|
|
777
1091
|
const view = await visibleDocs(effective);
|
|
778
|
-
return resolveDocument(view, id, effective) ??
|
|
1092
|
+
return resolveDocument(view, id, effective) ?? void 0;
|
|
779
1093
|
},
|
|
780
1094
|
getDocuments: async (ids, opts) => {
|
|
781
1095
|
const effective = opts?.perspective ?? perspective;
|
|
@@ -792,29 +1106,75 @@ function makeHandle(registry, projectId, dataset, accessControl, perspective) {
|
|
|
792
1106
|
},
|
|
793
1107
|
create: async (doc, opts) => {
|
|
794
1108
|
if (checkRevision(doc._id, opts?.ifRevisionId), store.get(doc._id) !== void 0)
|
|
795
|
-
throw
|
|
1109
|
+
throw fakeError(
|
|
1110
|
+
`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.`
|
|
1111
|
+
);
|
|
796
1112
|
const promoted = promote2(doc);
|
|
797
|
-
|
|
1113
|
+
await gate(promoted, "create"), await guardGate("create", null, promoted);
|
|
1114
|
+
const transactionId = opts?.transactionId ?? cryptoRandomKey();
|
|
1115
|
+
return opts?.dryRun !== !0 && (store.put(promoted), record(transactionId, [{ kind: "create", doc }])), writeResult(
|
|
1116
|
+
[promoted],
|
|
1117
|
+
[{ id: promoted._id, operation: "create" }],
|
|
1118
|
+
transactionId,
|
|
1119
|
+
opts
|
|
1120
|
+
);
|
|
798
1121
|
},
|
|
799
1122
|
createOrReplace: async (doc, opts) => {
|
|
800
1123
|
checkRevision(doc._id, opts?.ifRevisionId);
|
|
801
1124
|
const promoted = promote2(doc), existing = store.get(doc._id);
|
|
802
|
-
|
|
1125
|
+
await gate(existing ?? promoted, existing !== void 0 ? "update" : "create"), await guardGate(existing !== void 0 ? "update" : "create", existing ?? null, promoted);
|
|
1126
|
+
const transactionId = opts?.transactionId ?? cryptoRandomKey();
|
|
1127
|
+
opts?.dryRun !== !0 && (store.put(promoted), record(transactionId, [{ kind: "createOrReplace", doc }]));
|
|
1128
|
+
const operation = existing !== void 0 ? "update" : "create";
|
|
1129
|
+
return writeResult(
|
|
1130
|
+
[promoted],
|
|
1131
|
+
[{ id: promoted._id, operation }],
|
|
1132
|
+
transactionId,
|
|
1133
|
+
opts
|
|
1134
|
+
);
|
|
803
1135
|
},
|
|
804
1136
|
createIfNotExists: async (doc, opts) => {
|
|
805
1137
|
checkRevision(doc._id, opts?.ifRevisionId);
|
|
806
|
-
const existing = store.get(doc._id);
|
|
807
|
-
if (existing !== void 0)
|
|
1138
|
+
const existing = store.get(doc._id), transactionId = opts?.transactionId ?? cryptoRandomKey();
|
|
1139
|
+
if (existing !== void 0)
|
|
1140
|
+
return writeResult(
|
|
1141
|
+
[existing],
|
|
1142
|
+
[{ id: existing._id, operation: "none" }],
|
|
1143
|
+
transactionId,
|
|
1144
|
+
opts
|
|
1145
|
+
);
|
|
808
1146
|
const promoted = promote2(doc);
|
|
809
|
-
return await gate(promoted, "create"), store.put(promoted),
|
|
1147
|
+
return await gate(promoted, "create"), await guardGate("create", null, promoted), opts?.dryRun !== !0 && (store.put(promoted), record(transactionId, [{ kind: "createIfNotExists", doc }])), writeResult(
|
|
1148
|
+
[promoted],
|
|
1149
|
+
[{ id: promoted._id, operation: "create" }],
|
|
1150
|
+
transactionId,
|
|
1151
|
+
opts
|
|
1152
|
+
);
|
|
810
1153
|
},
|
|
811
1154
|
delete: async (id, opts) => {
|
|
812
1155
|
checkRevision(id, opts?.ifRevisionId);
|
|
813
1156
|
const existing = store.get(id);
|
|
814
|
-
|
|
1157
|
+
existing !== void 0 && await gate(existing, "update"), existing !== void 0 && await guardGate("delete", existing, null);
|
|
1158
|
+
const transactionId = opts?.transactionId ?? cryptoRandomKey();
|
|
1159
|
+
let removed = existing;
|
|
1160
|
+
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(
|
|
1161
|
+
removed !== void 0 ? [removed] : [],
|
|
1162
|
+
removed !== void 0 ? [{ id, operation: "delete" }] : [],
|
|
1163
|
+
transactionId,
|
|
1164
|
+
opts.returnDocuments ?? !0,
|
|
1165
|
+
opts.returnFirst ?? !0
|
|
1166
|
+
);
|
|
815
1167
|
},
|
|
816
|
-
patch: (documentId) => createPatch(documentId, {
|
|
1168
|
+
patch: (documentId) => createPatch(documentId, {
|
|
1169
|
+
apply: async (docId, ops, ifRev) => {
|
|
1170
|
+
const doc = await applyPatch(docId, ops, ifRev);
|
|
1171
|
+
return record(cryptoRandomKey(), [
|
|
1172
|
+
{ kind: "patch", documentId: docId, ops, ...ifRev !== void 0 ? { ifRev } : {} }
|
|
1173
|
+
]), doc;
|
|
1174
|
+
}
|
|
1175
|
+
}),
|
|
817
1176
|
transaction: () => createTransaction({ apply: applyTransaction }),
|
|
1177
|
+
mutate: async (operations, options) => applyTransaction(toTxnMutations(operations), options),
|
|
818
1178
|
action: async (action) => applyActions(store, action),
|
|
819
1179
|
releases: {
|
|
820
1180
|
create: async ({ releaseId, metadata }) => {
|
|
@@ -860,20 +1220,48 @@ function makeHandle(registry, projectId, dataset, accessControl, perspective) {
|
|
|
860
1220
|
...accessControl !== void 0 ? { request: makeRequestImpl(accessControl, projectId, dataset) } : {},
|
|
861
1221
|
config: () => ({ projectId, dataset, perspective }),
|
|
862
1222
|
withConfig: (config) => makeHandle(
|
|
863
|
-
|
|
1223
|
+
backend,
|
|
864
1224
|
config.projectId ?? projectId,
|
|
865
1225
|
config.dataset ?? dataset,
|
|
866
|
-
accessControl,
|
|
867
|
-
config.perspective ?? perspective
|
|
1226
|
+
config.accessControl ?? accessControl,
|
|
1227
|
+
config.perspective ?? perspective,
|
|
1228
|
+
mutationGuard
|
|
868
1229
|
),
|
|
869
1230
|
seed: (documents) => store.seed(documents),
|
|
870
1231
|
add: (documents) => store.add(documents),
|
|
871
1232
|
reset: () => store.reset(),
|
|
872
1233
|
snapshot: () => store.snapshot(),
|
|
873
1234
|
store: () => store,
|
|
874
|
-
datasets: () => [...registry.keys()].map(parseRegistryKey)
|
|
1235
|
+
datasets: () => [...registry.keys()].map(parseRegistryKey),
|
|
1236
|
+
stub: (name, handler) => {
|
|
1237
|
+
backend.stubs.set(name, handler);
|
|
1238
|
+
},
|
|
1239
|
+
stubs: () => Object.fromEntries(backend.stubs),
|
|
1240
|
+
mutations: () => backend.log.flatMap((entry) => entry.mutations),
|
|
1241
|
+
transactions: () => [...backend.log],
|
|
1242
|
+
clearLog: () => {
|
|
1243
|
+
backend.log.length = 0;
|
|
1244
|
+
},
|
|
1245
|
+
calls: (method) => method === void 0 ? [...backend.calls] : backend.calls.filter((c) => c.method === method),
|
|
1246
|
+
callCount: (method) => method === void 0 ? backend.calls.length : backend.calls.filter((c) => c.method === method).length,
|
|
1247
|
+
clearCalls: () => {
|
|
1248
|
+
backend.calls.length = 0;
|
|
1249
|
+
}
|
|
875
1250
|
};
|
|
1251
|
+
return wrapWithGuards(client, {
|
|
1252
|
+
getStub: (name) => backend.stubs.get(name),
|
|
1253
|
+
onCall: (method, args) => {
|
|
1254
|
+
const rec = { method, args, projectId, dataset };
|
|
1255
|
+
return backend.calls.push(rec), (outcome) => {
|
|
1256
|
+
"error" in outcome ? rec.error = outcome.error : rec.result = outcome.result;
|
|
1257
|
+
};
|
|
1258
|
+
},
|
|
1259
|
+
recorded: RECORDED_METHODS
|
|
1260
|
+
});
|
|
876
1261
|
}
|
|
1262
|
+
exports.ERR_PREFIX = ERR_PREFIX;
|
|
1263
|
+
exports.SUPPORTED_SURFACE = SUPPORTED_SURFACE;
|
|
1264
|
+
exports.UNSUPPORTED_MEMBERS = UNSUPPORTED_MEMBERS;
|
|
877
1265
|
exports.WriteAccessDeniedError = WriteAccessDeniedError;
|
|
878
1266
|
exports.applyPatchOps = applyPatchOps;
|
|
879
1267
|
exports.applyPerspective = applyPerspective;
|
|
@@ -883,10 +1271,12 @@ exports.createTestStore = createTestStore;
|
|
|
883
1271
|
exports.createTransaction = createTransaction;
|
|
884
1272
|
exports.defaultAclPath = defaultAclPath;
|
|
885
1273
|
exports.evaluateWriteAccess = evaluateWriteAccess;
|
|
1274
|
+
exports.fakeError = fakeError;
|
|
886
1275
|
exports.isDraftId = isDraftId;
|
|
887
1276
|
exports.isReleaseDocId = isReleaseDocId;
|
|
888
1277
|
exports.isSystemId = isSystemId;
|
|
889
1278
|
exports.isVersionId = isVersionId;
|
|
890
1279
|
exports.publishedIdOf = publishedIdOf;
|
|
891
1280
|
exports.releaseNameOf = releaseNameOf;
|
|
1281
|
+
exports.wrapWithGuards = wrapWithGuards;
|
|
892
1282
|
//# sourceMappingURL=index.cjs.map
|