@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/README.md +103 -26
- package/dist/index.cjs +862 -110
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +515 -35
- package/dist/index.d.ts +515 -35
- package/dist/index.js +863 -111
- 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,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
|
|
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
|
}
|
|
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)
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
|
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,
|
|
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(
|
|
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
|
|
346
|
-
|
|
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
|
-
})(),
|
|
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
|
|
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
|
|
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
|
|
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
|
|
371
|
-
`ifRevisionId check failed: document ${id}
|
|
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
|
|
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 =
|
|
1008
|
+
const promoted = promote2(mut.doc);
|
|
406
1009
|
if (store.get(promoted._id) !== void 0)
|
|
407
|
-
throw
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
transactionId
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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
|
|
468
|
-
|
|
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
|
|
1082
|
+
return doc === void 0 || !await canRead(doc) ? void 0 : doc;
|
|
475
1083
|
}
|
|
476
|
-
|
|
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
|
|
481
|
-
|
|
482
|
-
|
|
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 =
|
|
487
|
-
|
|
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)
|
|
493
|
-
|
|
494
|
-
|
|
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
|
-
|
|
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, {
|
|
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(
|
|
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
|