@sanity-labs/client-fake-for-test 0.7.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -5
- package/dist/index.cjs +280 -163
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +122 -24
- package/dist/index.d.ts +122 -24
- package/dist/index.js +280 -163
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -77,10 +77,10 @@ More patterns — passing the fake to code typed against `SanityClient`, asserti
|
|
|
77
77
|
| Transaction | `transaction()` chain → `.commit()` | Mutations: `.create`, `.createOrReplace`, `.createIfNotExists`, `.delete`, `.patch(id, recipe)`. Atomic — any throw rolls back the whole batch. Default result is a `MultipleMutationResult` (`{ transactionId, documentIds, results }`) |
|
|
78
78
|
| Actions | `action(action \| action[])` | Content Lake release / version actions, applied atomically |
|
|
79
79
|
| Releases | `releases.*`, `createVersion`, `unpublishVersion` | Content Releases lifecycle — create / edit / schedule / publish / archive / delete, plus version create / replace / discard / unpublish |
|
|
80
|
-
| Config | `config()` | Returns `{ projectId, dataset, perspective, token
|
|
81
|
-
| Config | `withConfig({ projectId?, dataset?, perspective?, accessControl?, token? })` | Sibling handle into the same multi-dataset registry; `token` binds a registered identity (see the token→identity registry below), `accessControl` rebinds it inline — the two are mutually exclusive |
|
|
80
|
+
| Config | `config()` | Returns `{ projectId, dataset, perspective, token?, useProjectHostname }` |
|
|
81
|
+
| Config | `withConfig({ projectId?, dataset?, perspective?, accessControl?, token?, useProjectHostname? })` | Sibling handle into the same multi-dataset registry; `token` binds a registered identity (see the token→identity registry below), `accessControl` rebinds it inline — the two are mutually exclusive; `useProjectHostname: false` mints a global-hosted sibling |
|
|
82
82
|
| Perspectives | `raw` / `published` / `drafts` / release stacks | Per-client default or per-`fetch` override; draft/version layering with `_originalId` projection |
|
|
83
|
-
| HTTP | `request({ url? \| uri? })` | Present **only** when the handle has an acting identity (`accessControl`, or a bound `token`). Serves `/users/me`
|
|
83
|
+
| HTTP | `request({ url? \| uri? })` | Present **only** when the handle has an acting identity (`accessControl`, or a bound `token`). Serves `/users/me` (host-aware: project-scoped record on the project host, `globalUser` on a global-hosted handle), the configured ACL path, and `/projects/<projectId>/users/<ids>` (project-user records with `id` + `sanityUserId`). Calling it without one throws a guidance error. |
|
|
84
84
|
|
|
85
85
|
#### Mutation return shapes
|
|
86
86
|
|
|
@@ -117,6 +117,9 @@ await client.patch("article-1").set({ title: "ok" }).commit(); // allowed
|
|
|
117
117
|
await expect(client.patch("article-1").set({ body: "no" }).commit()).rejects.toThrow();
|
|
118
118
|
```
|
|
119
119
|
|
|
120
|
+
> [!NOTE]
|
|
121
|
+
> Treat `before` / `after` as read-only — they are the live store images, not copies; mutating them corrupts the store.
|
|
122
|
+
|
|
120
123
|
#### Identity is the token
|
|
121
124
|
|
|
122
125
|
`identity` is the id of the handle's acting identity. To act as different users against the **same** store (e.g. to exercise `identity()`-based guards), register a token→identity registry and mint token-scoped siblings — the token determines identity, exactly like production:
|
|
@@ -137,8 +140,29 @@ const bob = alice.withConfig({ token: "t-bob" });
|
|
|
137
140
|
|
|
138
141
|
Register more identities after creation with `client.addIdentity(token, { user, grants })` — an upsert into the family-shared registry; already-bound siblings resolve lazily and pick the change up immediately. For a one-off identity without a registry, `withConfig({ accessControl })` still works (mutually exclusive with `token` in the same call).
|
|
139
142
|
|
|
140
|
-
|
|
141
|
-
|
|
143
|
+
#### Dual-namespace identities: global vs project user ids
|
|
144
|
+
|
|
145
|
+
In production the same token resolves to a **different project-scoped user id in every project** (what lake `identity()` returns) and **one account-global id** (what the global API host's `/users/me` returns); robot tokens carry a single id on every host. An identity can model that split with `globalUser` (served by `/users/me` on a `useProjectHostname: false` handle) and `usersByProject` (per-project records; the handle's project falls back to `user`):
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
const client = createTestClient({
|
|
149
|
+
projectId: "alpha",
|
|
150
|
+
token: "t-snorre",
|
|
151
|
+
identities: {
|
|
152
|
+
"t-snorre": {
|
|
153
|
+
user: { id: "pDefault" },
|
|
154
|
+
globalUser: { id: "gSnorre" },
|
|
155
|
+
usersByProject: { alpha: { id: "pAlpha" }, beta: { id: "pBeta" } },
|
|
156
|
+
grants,
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
await client.request({ uri: "/users/me" }); // { id: "pAlpha", ... }
|
|
161
|
+
await client.withConfig({ useProjectHostname: false }).request({ uri: "/users/me" }); // { id: "gSnorre", ... }
|
|
162
|
+
await client.request({ uri: "/projects/alpha/users/pAlpha" }); // { id: "pAlpha", sanityUserId: "gSnorre", ... } (ProjectUserRecord)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Grant-filter and mutation-guard `identity()` stay bound to the **project-scoped** record, matching the lake. Identities without `globalUser` serve one record everywhere — the production robot-token shape.
|
|
142
166
|
|
|
143
167
|
### Test-only helpers (not on real `SanityClient`)
|
|
144
168
|
|
package/dist/index.cjs
CHANGED
|
@@ -61,69 +61,65 @@ async function enforceWriteAccess(args) {
|
|
|
61
61
|
function defaultAclPath(projectId, dataset) {
|
|
62
62
|
return `/projects/${projectId}/datasets/${dataset}/acl`;
|
|
63
63
|
}
|
|
64
|
-
function
|
|
64
|
+
function projectUserOf(identity, projectId) {
|
|
65
|
+
return identity.usersByProject?.[projectId] ?? identity.user;
|
|
66
|
+
}
|
|
67
|
+
function serveProjectUsers(args) {
|
|
68
|
+
const { requestedIds, pathProjectId, identities } = args, toRecord = (identity) => {
|
|
69
|
+
const projectUser = projectUserOf(identity, pathProjectId);
|
|
70
|
+
return {
|
|
71
|
+
id: projectUser.id,
|
|
72
|
+
sanityUserId: identity.globalUser?.id ?? projectUser.id,
|
|
73
|
+
displayName: projectUser.name ?? projectUser.id,
|
|
74
|
+
...projectUser.email !== void 0 ? { email: projectUser.email } : {}
|
|
75
|
+
};
|
|
76
|
+
}, byId = /* @__PURE__ */ new Map();
|
|
77
|
+
for (const identity of identities) {
|
|
78
|
+
const resolvedId = projectUserOf(identity, pathProjectId).id, existing = byId.get(resolvedId);
|
|
79
|
+
if (existing !== void 0 && JSON.stringify(toRecord(existing)) !== JSON.stringify(toRecord(identity)))
|
|
80
|
+
throw fakeError(
|
|
81
|
+
`request(): two registered identities resolve to project user "${resolvedId}" in project "${pathProjectId}" with different records. The directory cannot answer unambiguously \u2014 fix the identity registrations so duplicate ids carry identical records.`
|
|
82
|
+
);
|
|
83
|
+
byId.set(resolvedId, identity);
|
|
84
|
+
}
|
|
85
|
+
const ids = requestedIds.split(","), [firstId] = ids;
|
|
86
|
+
if (ids.length === 1 && firstId !== void 0) {
|
|
87
|
+
const identity = byId.get(firstId);
|
|
88
|
+
if (identity === void 0)
|
|
89
|
+
throw Object.assign(
|
|
90
|
+
fakeError(
|
|
91
|
+
`request(): no registered identity resolves to project user "${firstId}" in project "${pathProjectId}". Register it via createTestClient({ identities }) or addIdentity().`
|
|
92
|
+
),
|
|
93
|
+
{ statusCode: 404 }
|
|
94
|
+
);
|
|
95
|
+
return toRecord(identity);
|
|
96
|
+
}
|
|
97
|
+
return ids.flatMap((id) => {
|
|
98
|
+
const identity = byId.get(id);
|
|
99
|
+
return identity === void 0 ? [] : [toRecord(identity)];
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
const PROJECT_USERS_PATH = /^\/projects\/([^/]+)\/users\/([^/]+)$/;
|
|
103
|
+
function makeRequestImpl(ctx) {
|
|
104
|
+
const { getAccess, projectId, dataset, useProjectHostname, listIdentities } = ctx;
|
|
65
105
|
return async (opts) => {
|
|
66
106
|
const accessControl = getAccess(), aclPath = accessControl.aclPath ?? defaultAclPath(projectId, dataset), path = opts.uri ?? opts.url;
|
|
67
107
|
if (path === "/users/me")
|
|
68
|
-
return accessControl.currentUser;
|
|
108
|
+
return useProjectHostname ? accessControl.currentUser : accessControl.globalUser ?? accessControl.currentUser;
|
|
69
109
|
if (path === aclPath)
|
|
70
110
|
return accessControl.grants;
|
|
111
|
+
const projectUsers = path?.match(PROJECT_USERS_PATH), [, pathProjectId, requestedIds] = projectUsers ?? [];
|
|
112
|
+
if (pathProjectId !== void 0 && requestedIds !== void 0)
|
|
113
|
+
return serveProjectUsers({
|
|
114
|
+
requestedIds,
|
|
115
|
+
pathProjectId,
|
|
116
|
+
identities: listIdentities()
|
|
117
|
+
});
|
|
71
118
|
throw fakeError(
|
|
72
|
-
`request(): no handler for path "${path}". With access control configured, the fake
|
|
119
|
+
`request(): no handler for path "${path}". With access control configured, the fake serves "/users/me", "${aclPath}", and "/projects/<projectId>/users/<ids>". For any other endpoint, register a stub (client.stub("request", fn)) or intercept the HTTP call with a tool like MSW.`
|
|
73
120
|
);
|
|
74
121
|
};
|
|
75
122
|
}
|
|
76
|
-
function createTestStore(initial) {
|
|
77
|
-
const docs = /* @__PURE__ */ new Map(), promote2 = (doc) => {
|
|
78
|
-
if (typeof doc._id != "string" || doc._id.length === 0)
|
|
79
|
-
throw fakeError(
|
|
80
|
-
`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)}`
|
|
81
|
-
);
|
|
82
|
-
if (typeof doc._type != "string" || doc._type.length === 0)
|
|
83
|
-
throw fakeError(
|
|
84
|
-
`Document "${doc._id}" is missing a non-empty string \`_type\` (got ${JSON.stringify(doc._type)}). Every Sanity document needs a _type.`
|
|
85
|
-
);
|
|
86
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
87
|
-
return {
|
|
88
|
-
...doc,
|
|
89
|
-
_id: doc._id,
|
|
90
|
-
_type: doc._type,
|
|
91
|
-
_rev: doc._rev ?? cryptoRandomKey(),
|
|
92
|
-
_createdAt: doc._createdAt ?? now,
|
|
93
|
-
_updatedAt: doc._updatedAt ?? now
|
|
94
|
-
};
|
|
95
|
-
}, seed = (documents) => {
|
|
96
|
-
docs.clear();
|
|
97
|
-
for (const d of documents) {
|
|
98
|
-
const promoted = promote2(d);
|
|
99
|
-
docs.set(promoted._id, promoted);
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
return initial && seed(initial), {
|
|
103
|
-
seed,
|
|
104
|
-
add: (documents) => {
|
|
105
|
-
for (const d of documents) {
|
|
106
|
-
const promoted = promote2(d);
|
|
107
|
-
docs.set(promoted._id, promoted);
|
|
108
|
-
}
|
|
109
|
-
},
|
|
110
|
-
reset: () => docs.clear(),
|
|
111
|
-
all: () => [...docs.values()],
|
|
112
|
-
get: (id) => docs.get(id),
|
|
113
|
-
put: (doc) => {
|
|
114
|
-
docs.set(doc._id, doc);
|
|
115
|
-
},
|
|
116
|
-
remove: (id) => {
|
|
117
|
-
const doc = docs.get(id);
|
|
118
|
-
return doc && docs.delete(id), doc;
|
|
119
|
-
},
|
|
120
|
-
snapshot: () => [...docs.values()]
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
function cryptoRandomKey(length = 12) {
|
|
124
|
-
const bytes = new Uint8Array(length);
|
|
125
|
-
return globalThis.crypto.getRandomValues(bytes), [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, length);
|
|
126
|
-
}
|
|
127
123
|
function describeType(value) {
|
|
128
124
|
return value === null ? "null" : Array.isArray(value) ? "an array" : `a ${typeof value}`;
|
|
129
125
|
}
|
|
@@ -326,13 +322,140 @@ function insertIntoArray(doc, position, anchor, items) {
|
|
|
326
322
|
const idx = resolveSelector(arr, last.selector);
|
|
327
323
|
position === "replace" ? arr.splice(idx, 1, ...items) : position === "before" ? arr.splice(idx, 0, ...items) : arr.splice(idx + 1, 0, ...items);
|
|
328
324
|
}
|
|
329
|
-
const
|
|
325
|
+
const DRAFTS_PREFIX = "drafts.", VERSIONS_PREFIX$1 = "versions.", RELEASE_DOC_PREFIX$1 = "_.releases.";
|
|
326
|
+
function publishedIdOf(id) {
|
|
327
|
+
if (id.startsWith(DRAFTS_PREFIX)) return id.slice(DRAFTS_PREFIX.length);
|
|
328
|
+
if (id.startsWith(VERSIONS_PREFIX$1)) {
|
|
329
|
+
const rest = id.slice(VERSIONS_PREFIX$1.length), dot = rest.indexOf(".");
|
|
330
|
+
return dot === -1 ? rest : rest.slice(dot + 1);
|
|
331
|
+
}
|
|
332
|
+
return id;
|
|
333
|
+
}
|
|
334
|
+
function releaseNameOf(id) {
|
|
335
|
+
if (!id.startsWith(VERSIONS_PREFIX$1)) return;
|
|
336
|
+
const rest = id.slice(VERSIONS_PREFIX$1.length), dot = rest.indexOf(".");
|
|
337
|
+
if (dot !== -1)
|
|
338
|
+
return rest.slice(0, dot);
|
|
339
|
+
}
|
|
340
|
+
const isDraftId = (id) => id.startsWith(DRAFTS_PREFIX), isVersionId = (id) => id.startsWith(VERSIONS_PREFIX$1), isSystemId = (id) => id.startsWith("_."), isReleaseDocId = (id) => id.startsWith(RELEASE_DOC_PREFIX$1);
|
|
341
|
+
function applyPerspective(docs, perspective) {
|
|
342
|
+
if (perspective === "raw") return [...docs];
|
|
343
|
+
if (perspective === "published")
|
|
344
|
+
return docs.filter((d) => !isDraftId(d._id) && !isVersionId(d._id) && !isSystemId(d._id));
|
|
345
|
+
if (perspective === "drafts")
|
|
346
|
+
return mergeStack(docs, [], { includeDrafts: !0, includeReleaseDocs: !1 });
|
|
347
|
+
const stack = [];
|
|
348
|
+
let includeDrafts = !1;
|
|
349
|
+
for (const entry of perspective)
|
|
350
|
+
entry === "drafts" ? includeDrafts = !0 : stack.push(entry);
|
|
351
|
+
return mergeStack(docs, stack, { includeDrafts, includeReleaseDocs: !0 });
|
|
352
|
+
}
|
|
353
|
+
function mergeStack(docs, releaseStack, options) {
|
|
354
|
+
const groups = /* @__PURE__ */ new Map(), systemReleases = [];
|
|
355
|
+
for (const doc of docs) {
|
|
356
|
+
if (isReleaseDocId(doc._id)) {
|
|
357
|
+
options.includeReleaseDocs && systemReleases.push(doc);
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
if (isSystemId(doc._id)) continue;
|
|
361
|
+
const pubId = publishedIdOf(doc._id);
|
|
362
|
+
let group = groups.get(pubId);
|
|
363
|
+
if (group === void 0 && (group = { versions: /* @__PURE__ */ new Map() }, groups.set(pubId, group)), isDraftId(doc._id))
|
|
364
|
+
group.draft = doc;
|
|
365
|
+
else if (isVersionId(doc._id)) {
|
|
366
|
+
const releaseName = releaseNameOf(doc._id);
|
|
367
|
+
releaseName !== void 0 && group.versions.set(releaseName, doc);
|
|
368
|
+
} else
|
|
369
|
+
group.published = doc;
|
|
370
|
+
}
|
|
371
|
+
const out = [];
|
|
372
|
+
for (const [pubId, group] of groups) {
|
|
373
|
+
let chosen;
|
|
374
|
+
for (const releaseName of releaseStack) {
|
|
375
|
+
const v = group.versions.get(releaseName);
|
|
376
|
+
if (v !== void 0) {
|
|
377
|
+
if (isUnpublishStub(v)) {
|
|
378
|
+
chosen = void 0;
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
chosen = v;
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
chosen === void 0 && options.includeDrafts && group.draft !== void 0 && (chosen = group.draft), chosen === void 0 && group.published !== void 0 && (releaseStack.some((name) => {
|
|
386
|
+
const v = group.versions.get(name);
|
|
387
|
+
return v !== void 0 && isUnpublishStub(v);
|
|
388
|
+
}) || (chosen = group.published)), chosen !== void 0 && out.push(projectToPublishedId(chosen, pubId));
|
|
389
|
+
}
|
|
390
|
+
return [...out, ...systemReleases];
|
|
391
|
+
}
|
|
392
|
+
function isUnpublishStub(doc) {
|
|
393
|
+
return doc._system?.delete === !0;
|
|
394
|
+
}
|
|
395
|
+
function projectToPublishedId(doc, pubId) {
|
|
396
|
+
return doc._id === pubId ? doc : { ...doc, _id: pubId, _originalId: doc._id };
|
|
397
|
+
}
|
|
398
|
+
function resolveDocument(docs, id, perspective) {
|
|
399
|
+
return perspective === "raw" ? docs.find((d) => d._id === id) : applyPerspective(docs, perspective).find((d) => d._id === id);
|
|
400
|
+
}
|
|
401
|
+
function createTestStore(initial) {
|
|
402
|
+
const docs = /* @__PURE__ */ new Map(), promote2 = (doc) => {
|
|
403
|
+
if (typeof doc._id != "string" || doc._id.length === 0)
|
|
404
|
+
throw fakeError(
|
|
405
|
+
`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)}`
|
|
406
|
+
);
|
|
407
|
+
if (typeof doc._type != "string" || doc._type.length === 0)
|
|
408
|
+
throw fakeError(
|
|
409
|
+
`Document "${doc._id}" is missing a non-empty string \`_type\` (got ${JSON.stringify(doc._type)}). Every Sanity document needs a _type.`
|
|
410
|
+
);
|
|
411
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
412
|
+
return {
|
|
413
|
+
...doc,
|
|
414
|
+
_id: doc._id,
|
|
415
|
+
_type: doc._type,
|
|
416
|
+
_rev: doc._rev ?? cryptoRandomKey(),
|
|
417
|
+
_createdAt: doc._createdAt ?? now,
|
|
418
|
+
_updatedAt: doc._updatedAt ?? now
|
|
419
|
+
};
|
|
420
|
+
}, seed = (documents) => {
|
|
421
|
+
docs.clear();
|
|
422
|
+
for (const d of documents) {
|
|
423
|
+
const promoted = promote2(d);
|
|
424
|
+
docs.set(promoted._id, promoted);
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
return initial && seed(initial), {
|
|
428
|
+
seed,
|
|
429
|
+
add: (documents) => {
|
|
430
|
+
for (const d of documents) {
|
|
431
|
+
const promoted = promote2(d);
|
|
432
|
+
docs.set(promoted._id, promoted);
|
|
433
|
+
}
|
|
434
|
+
},
|
|
435
|
+
reset: () => docs.clear(),
|
|
436
|
+
all: () => [...docs.values()],
|
|
437
|
+
get: (id) => docs.get(id),
|
|
438
|
+
put: (doc) => {
|
|
439
|
+
docs.set(doc._id, doc);
|
|
440
|
+
},
|
|
441
|
+
remove: (id) => {
|
|
442
|
+
const doc = docs.get(id);
|
|
443
|
+
return doc && docs.delete(id), doc;
|
|
444
|
+
},
|
|
445
|
+
snapshot: () => [...docs.values()]
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
function cryptoRandomKey(length = 12) {
|
|
449
|
+
const bytes = new Uint8Array(length);
|
|
450
|
+
return globalThis.crypto.getRandomValues(bytes), [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, length);
|
|
451
|
+
}
|
|
452
|
+
const RELEASE_DOC_PREFIX = "_.releases.", VERSIONS_PREFIX = "versions.", releaseDocId = (releaseId) => `${RELEASE_DOC_PREFIX}${releaseId}`, versionDocId = (releaseId, publishedId) => `${VERSIONS_PREFIX}${releaseId}.${publishedId}`;
|
|
330
453
|
function parseVersionId(versionId) {
|
|
331
|
-
if (!versionId.startsWith(VERSIONS_PREFIX
|
|
454
|
+
if (!versionId.startsWith(VERSIONS_PREFIX))
|
|
332
455
|
throw fakeError(
|
|
333
456
|
`"${versionId}" is not a version document id. Version ids look like "versions.<releaseId>.<publishedId>" (e.g. "versions.summer-drop.product-1").`
|
|
334
457
|
);
|
|
335
|
-
const rest = versionId.slice(VERSIONS_PREFIX
|
|
458
|
+
const rest = versionId.slice(VERSIONS_PREFIX.length), dot = rest.indexOf(".");
|
|
336
459
|
if (dot === -1)
|
|
337
460
|
throw fakeError(
|
|
338
461
|
`"${versionId}" is not a version document id. Version ids look like "versions.<releaseId>.<publishedId>" (e.g. "versions.summer-drop.product-1").`
|
|
@@ -378,7 +501,7 @@ function requireActive(release) {
|
|
|
378
501
|
`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.`
|
|
379
502
|
);
|
|
380
503
|
}
|
|
381
|
-
function applyAction(store, action) {
|
|
504
|
+
async function applyAction(store, action, hooks) {
|
|
382
505
|
switch (action.actionType) {
|
|
383
506
|
case "sanity.action.release.create": {
|
|
384
507
|
if (maybeRelease(store, action.releaseId) !== void 0)
|
|
@@ -436,7 +559,7 @@ function applyAction(store, action) {
|
|
|
436
559
|
throw fakeError(
|
|
437
560
|
`Cannot publish release "${action.releaseId}": it is "${release.state}", and only "active" or "scheduled" releases can be published.`
|
|
438
561
|
);
|
|
439
|
-
const prefix = `${VERSIONS_PREFIX
|
|
562
|
+
const prefix = `${VERSIONS_PREFIX}${action.releaseId}.`, sources = store.all().filter((d) => d._id.startsWith(prefix));
|
|
440
563
|
for (const v of sources) {
|
|
441
564
|
const pubId = v._id.slice(prefix.length);
|
|
442
565
|
if (v._system?.delete === !0)
|
|
@@ -476,7 +599,7 @@ function applyAction(store, action) {
|
|
|
476
599
|
throw fakeError(
|
|
477
600
|
`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.`
|
|
478
601
|
);
|
|
479
|
-
const prefix = `${VERSIONS_PREFIX
|
|
602
|
+
const prefix = `${VERSIONS_PREFIX}${action.releaseId}.`;
|
|
480
603
|
for (const v of store.all())
|
|
481
604
|
v._id.startsWith(prefix) && store.remove(v._id);
|
|
482
605
|
return store.remove(release._id), { transactionId: cryptoRandomKey() };
|
|
@@ -520,9 +643,46 @@ function applyAction(store, action) {
|
|
|
520
643
|
};
|
|
521
644
|
return store.put(promote(doc)), { transactionId: cryptoRandomKey() };
|
|
522
645
|
}
|
|
646
|
+
case "sanity.action.document.publish": {
|
|
647
|
+
requireDraftOf(action);
|
|
648
|
+
const draft = store.get(action.draftId);
|
|
649
|
+
if (draft === void 0)
|
|
650
|
+
throw fakeError(
|
|
651
|
+
`Cannot publish "${action.publishedId}": draft "${action.draftId}" not found. document.publish promotes an existing draft onto the published id \u2014 create the draft first (e.g. client.createOrReplace({ _id: "${action.draftId}", ... })).`
|
|
652
|
+
);
|
|
653
|
+
const published = store.get(action.publishedId);
|
|
654
|
+
requireRevisionMatch(action.draftId, draft._rev, action.ifDraftRevisionId), requireRevisionMatch(action.publishedId, published?._rev, action.ifPublishedRevisionId);
|
|
655
|
+
const promoted = promote({ ...draft, _id: action.publishedId });
|
|
656
|
+
return published !== void 0 && (promoted._createdAt = published._createdAt), await hooks?.gate?.(promoted, published === void 0 ? "create" : "update"), await hooks?.guard?.("publish", published ?? null, promoted), store.put(promoted), store.remove(action.draftId), { transactionId: cryptoRandomKey() };
|
|
657
|
+
}
|
|
658
|
+
case "sanity.action.document.unpublish": {
|
|
659
|
+
requireDraftOf(action);
|
|
660
|
+
const published = store.get(action.publishedId);
|
|
661
|
+
if (published === void 0)
|
|
662
|
+
throw fakeError(
|
|
663
|
+
`Cannot unpublish "${action.publishedId}": no published document with that _id exists. document.unpublish retracts a live published document into "${action.draftId}".`
|
|
664
|
+
);
|
|
665
|
+
if (await hooks?.gate?.(published, "update"), await hooks?.guard?.("unpublish", published, null), store.get(action.draftId) === void 0) {
|
|
666
|
+
const draft = promote({ ...published, _id: action.draftId });
|
|
667
|
+
draft._createdAt = published._createdAt, store.put(draft);
|
|
668
|
+
}
|
|
669
|
+
return store.remove(action.publishedId), { transactionId: cryptoRandomKey() };
|
|
670
|
+
}
|
|
523
671
|
}
|
|
524
672
|
}
|
|
525
|
-
function
|
|
673
|
+
function requireDraftOf(action) {
|
|
674
|
+
if (!(publishedIdOf(action.draftId) === action.publishedId && action.draftId !== action.publishedId))
|
|
675
|
+
throw fakeError(
|
|
676
|
+
`${action.actionType}: draftId "${action.draftId}" is not the draft of publishedId "${action.publishedId}" \u2014 expected draftId "drafts.${action.publishedId}".`
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
function requireRevisionMatch(documentId, actualRev, expectedRev) {
|
|
680
|
+
if (!(expectedRev === void 0 || actualRev === expectedRev))
|
|
681
|
+
throw fakeError(
|
|
682
|
+
`Revision precondition failed for "${documentId}": expected _rev "${expectedRev}" but the document is at ${actualRev === void 0 ? "no revision (missing)" : `"${actualRev}"`}.`
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
async function applyActions(store, actions, hooks) {
|
|
526
686
|
const list = Array.isArray(actions) ? actions : [actions], snapshot = /* @__PURE__ */ new Map(), remember = (id) => {
|
|
527
687
|
snapshot.has(id) || snapshot.set(id, store.get(id));
|
|
528
688
|
};
|
|
@@ -537,11 +697,11 @@ function applyActions(store, actions) {
|
|
|
537
697
|
case "sanity.action.release.archive":
|
|
538
698
|
case "sanity.action.release.unarchive":
|
|
539
699
|
case "sanity.action.release.delete":
|
|
540
|
-
remember(`${RELEASE_DOC_PREFIX
|
|
700
|
+
remember(`${RELEASE_DOC_PREFIX}${a.releaseId}`);
|
|
541
701
|
break;
|
|
542
702
|
case "sanity.action.release.publish": {
|
|
543
|
-
remember(`${RELEASE_DOC_PREFIX
|
|
544
|
-
const prefix = `${VERSIONS_PREFIX
|
|
703
|
+
remember(`${RELEASE_DOC_PREFIX}${a.releaseId}`);
|
|
704
|
+
const prefix = `${VERSIONS_PREFIX}${a.releaseId}.`;
|
|
545
705
|
for (const d of store.all())
|
|
546
706
|
d._id.startsWith(prefix) && (remember(d._id), remember(d._id.slice(prefix.length)));
|
|
547
707
|
break;
|
|
@@ -558,8 +718,12 @@ function applyActions(store, actions) {
|
|
|
558
718
|
case "sanity.action.document.version.unpublish":
|
|
559
719
|
remember(a.versionId);
|
|
560
720
|
break;
|
|
721
|
+
case "sanity.action.document.publish":
|
|
722
|
+
case "sanity.action.document.unpublish":
|
|
723
|
+
remember(a.draftId), remember(a.publishedId);
|
|
724
|
+
break;
|
|
561
725
|
}
|
|
562
|
-
last = applyAction(store, a);
|
|
726
|
+
last = await applyAction(store, a, hooks);
|
|
563
727
|
}
|
|
564
728
|
return last;
|
|
565
729
|
} catch (err) {
|
|
@@ -568,82 +732,6 @@ function applyActions(store, actions) {
|
|
|
568
732
|
throw err;
|
|
569
733
|
}
|
|
570
734
|
}
|
|
571
|
-
const DRAFTS_PREFIX = "drafts.", VERSIONS_PREFIX = "versions.", RELEASE_DOC_PREFIX = "_.releases.";
|
|
572
|
-
function publishedIdOf(id) {
|
|
573
|
-
if (id.startsWith(DRAFTS_PREFIX)) return id.slice(DRAFTS_PREFIX.length);
|
|
574
|
-
if (id.startsWith(VERSIONS_PREFIX)) {
|
|
575
|
-
const rest = id.slice(VERSIONS_PREFIX.length), dot = rest.indexOf(".");
|
|
576
|
-
return dot === -1 ? rest : rest.slice(dot + 1);
|
|
577
|
-
}
|
|
578
|
-
return id;
|
|
579
|
-
}
|
|
580
|
-
function releaseNameOf(id) {
|
|
581
|
-
if (!id.startsWith(VERSIONS_PREFIX)) return;
|
|
582
|
-
const rest = id.slice(VERSIONS_PREFIX.length), dot = rest.indexOf(".");
|
|
583
|
-
if (dot !== -1)
|
|
584
|
-
return rest.slice(0, dot);
|
|
585
|
-
}
|
|
586
|
-
const isDraftId = (id) => id.startsWith(DRAFTS_PREFIX), isVersionId = (id) => id.startsWith(VERSIONS_PREFIX), isSystemId = (id) => id.startsWith("_."), isReleaseDocId = (id) => id.startsWith(RELEASE_DOC_PREFIX);
|
|
587
|
-
function applyPerspective(docs, perspective) {
|
|
588
|
-
if (perspective === "raw") return [...docs];
|
|
589
|
-
if (perspective === "published")
|
|
590
|
-
return docs.filter((d) => !isDraftId(d._id) && !isVersionId(d._id) && !isSystemId(d._id));
|
|
591
|
-
if (perspective === "drafts")
|
|
592
|
-
return mergeStack(docs, [], { includeDrafts: !0, includeReleaseDocs: !1 });
|
|
593
|
-
const stack = [];
|
|
594
|
-
let includeDrafts = !1;
|
|
595
|
-
for (const entry of perspective)
|
|
596
|
-
entry === "drafts" ? includeDrafts = !0 : stack.push(entry);
|
|
597
|
-
return mergeStack(docs, stack, { includeDrafts, includeReleaseDocs: !0 });
|
|
598
|
-
}
|
|
599
|
-
function mergeStack(docs, releaseStack, options) {
|
|
600
|
-
const groups = /* @__PURE__ */ new Map(), systemReleases = [];
|
|
601
|
-
for (const doc of docs) {
|
|
602
|
-
if (isReleaseDocId(doc._id)) {
|
|
603
|
-
options.includeReleaseDocs && systemReleases.push(doc);
|
|
604
|
-
continue;
|
|
605
|
-
}
|
|
606
|
-
if (isSystemId(doc._id)) continue;
|
|
607
|
-
const pubId = publishedIdOf(doc._id);
|
|
608
|
-
let group = groups.get(pubId);
|
|
609
|
-
if (group === void 0 && (group = { versions: /* @__PURE__ */ new Map() }, groups.set(pubId, group)), isDraftId(doc._id))
|
|
610
|
-
group.draft = doc;
|
|
611
|
-
else if (isVersionId(doc._id)) {
|
|
612
|
-
const releaseName = releaseNameOf(doc._id);
|
|
613
|
-
releaseName !== void 0 && group.versions.set(releaseName, doc);
|
|
614
|
-
} else
|
|
615
|
-
group.published = doc;
|
|
616
|
-
}
|
|
617
|
-
const out = [];
|
|
618
|
-
for (const [pubId, group] of groups) {
|
|
619
|
-
let chosen;
|
|
620
|
-
for (const releaseName of releaseStack) {
|
|
621
|
-
const v = group.versions.get(releaseName);
|
|
622
|
-
if (v !== void 0) {
|
|
623
|
-
if (isUnpublishStub(v)) {
|
|
624
|
-
chosen = void 0;
|
|
625
|
-
break;
|
|
626
|
-
}
|
|
627
|
-
chosen = v;
|
|
628
|
-
break;
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
chosen === void 0 && options.includeDrafts && group.draft !== void 0 && (chosen = group.draft), chosen === void 0 && group.published !== void 0 && (releaseStack.some((name) => {
|
|
632
|
-
const v = group.versions.get(name);
|
|
633
|
-
return v !== void 0 && isUnpublishStub(v);
|
|
634
|
-
}) || (chosen = group.published)), chosen !== void 0 && out.push(projectToPublishedId(chosen, pubId));
|
|
635
|
-
}
|
|
636
|
-
return [...out, ...systemReleases];
|
|
637
|
-
}
|
|
638
|
-
function isUnpublishStub(doc) {
|
|
639
|
-
return doc._system?.delete === !0;
|
|
640
|
-
}
|
|
641
|
-
function projectToPublishedId(doc, pubId) {
|
|
642
|
-
return doc._id === pubId ? doc : { ...doc, _id: pubId, _originalId: doc._id };
|
|
643
|
-
}
|
|
644
|
-
function resolveDocument(docs, id, perspective) {
|
|
645
|
-
return perspective === "raw" ? docs.find((d) => d._id === id) : applyPerspective(docs, perspective).find((d) => d._id === id);
|
|
646
|
-
}
|
|
647
735
|
function createTransaction(executor) {
|
|
648
736
|
const mutations = [], handle = {
|
|
649
737
|
create: (doc) => (mutations.push({ kind: "create", doc }), handle),
|
|
@@ -739,7 +827,7 @@ const ISSUES_URL = "https://github.com/sanity-labs/client-fake-for-test/issues",
|
|
|
739
827
|
},
|
|
740
828
|
request: {
|
|
741
829
|
what: "generic HTTP request hook \u2014 only wired up when the handle has an acting identity",
|
|
742
|
-
workaround: "Create the client with `accessControl`, or bind a registered token (`identities` + `token` / `withConfig({ token })`), to enable request() (it serves /users/me
|
|
830
|
+
workaround: "Create the client with `accessControl`, or bind a registered token (`identities` + `token` / `withConfig({ token })`), to enable request() (it serves /users/me, the ACL path, and /projects/<projectId>/users/<ids>). Or register a stub."
|
|
743
831
|
},
|
|
744
832
|
dryRun: {
|
|
745
833
|
what: "not a method \u2014 did you mean the `{ dryRun }` mutation option?",
|
|
@@ -922,15 +1010,16 @@ function createTestClient(options = {}) {
|
|
|
922
1010
|
const { projectId, dataset } = parseRegistryKey(key);
|
|
923
1011
|
ensureStore(projectId, dataset).seed(docs);
|
|
924
1012
|
}
|
|
925
|
-
return makeHandle(
|
|
1013
|
+
return makeHandle({
|
|
926
1014
|
backend,
|
|
927
|
-
defaultProjectId,
|
|
928
|
-
defaultDataset,
|
|
929
|
-
options.accessControl,
|
|
930
|
-
options.perspective ?? "raw",
|
|
931
|
-
options.mutationGuard,
|
|
932
|
-
options.token
|
|
933
|
-
|
|
1015
|
+
projectId: defaultProjectId,
|
|
1016
|
+
dataset: defaultDataset,
|
|
1017
|
+
accessControl: options.accessControl,
|
|
1018
|
+
perspective: options.perspective ?? "raw",
|
|
1019
|
+
mutationGuard: options.mutationGuard,
|
|
1020
|
+
token: options.token,
|
|
1021
|
+
useProjectHostname: options.useProjectHostname ?? !0
|
|
1022
|
+
});
|
|
934
1023
|
}
|
|
935
1024
|
function dedupeIds(results) {
|
|
936
1025
|
const seen = /* @__PURE__ */ new Set(), out = [];
|
|
@@ -982,13 +1071,26 @@ function toTxnMutations(operations) {
|
|
|
982
1071
|
}
|
|
983
1072
|
];
|
|
984
1073
|
}
|
|
985
|
-
function makeHandle(
|
|
986
|
-
const {
|
|
1074
|
+
function makeHandle(handleArgs) {
|
|
1075
|
+
const {
|
|
1076
|
+
backend,
|
|
1077
|
+
projectId,
|
|
1078
|
+
dataset,
|
|
1079
|
+
accessControl,
|
|
1080
|
+
perspective,
|
|
1081
|
+
mutationGuard,
|
|
1082
|
+
token,
|
|
1083
|
+
useProjectHostname
|
|
1084
|
+
} = handleArgs, { registry } = backend, activeAccess = () => {
|
|
987
1085
|
if (token === void 0) return accessControl;
|
|
988
1086
|
const identity = backend.identities.get(token);
|
|
989
1087
|
if (identity === void 0)
|
|
990
1088
|
throw fakeError(`no identity is registered for token "${token}" (was it removed?).`);
|
|
991
|
-
return {
|
|
1089
|
+
return {
|
|
1090
|
+
currentUser: projectUserOf(identity, projectId),
|
|
1091
|
+
...identity.globalUser !== void 0 ? { globalUser: identity.globalUser } : {},
|
|
1092
|
+
grants: identity.grants
|
|
1093
|
+
};
|
|
992
1094
|
}, requireActiveAccess = () => {
|
|
993
1095
|
const access = activeAccess();
|
|
994
1096
|
if (access === void 0)
|
|
@@ -1138,7 +1240,7 @@ function makeHandle(backend, projectId, dataset, accessControl, perspective, mut
|
|
|
1138
1240
|
txnOptions?.returnDocuments ?? !1,
|
|
1139
1241
|
txnOptions?.returnFirst ?? !1
|
|
1140
1242
|
);
|
|
1141
|
-
}, runAction = (actions) => serializeWrite(backend, async () => applyActions(store, actions)), accessVisibleDocs = async () => {
|
|
1243
|
+
}, runAction = (actions) => serializeWrite(backend, async () => applyActions(store, actions, { gate, guard: guardGate })), accessVisibleDocs = async () => {
|
|
1142
1244
|
const all = store.all();
|
|
1143
1245
|
if (activeAccess() === void 0) return all;
|
|
1144
1246
|
const out = [];
|
|
@@ -1309,20 +1411,35 @@ function makeHandle(backend, projectId, dataset, accessControl, perspective, mut
|
|
|
1309
1411
|
versionId: `versions.${releaseId}.${publishedId}`,
|
|
1310
1412
|
publishedId
|
|
1311
1413
|
}),
|
|
1312
|
-
...token !== void 0 || accessControl !== void 0 ? {
|
|
1313
|
-
|
|
1414
|
+
...token !== void 0 || accessControl !== void 0 ? {
|
|
1415
|
+
request: makeRequestImpl({
|
|
1416
|
+
getAccess: requireActiveAccess,
|
|
1417
|
+
projectId,
|
|
1418
|
+
dataset,
|
|
1419
|
+
useProjectHostname,
|
|
1420
|
+
listIdentities: () => [...backend.identities.values()]
|
|
1421
|
+
})
|
|
1422
|
+
} : {},
|
|
1423
|
+
config: () => ({
|
|
1424
|
+
projectId,
|
|
1425
|
+
dataset,
|
|
1426
|
+
perspective,
|
|
1427
|
+
...token !== void 0 ? { token } : {},
|
|
1428
|
+
useProjectHostname
|
|
1429
|
+
}),
|
|
1314
1430
|
withConfig: (config) => {
|
|
1315
1431
|
rejectAmbiguousIdentity("withConfig()", config), config.token !== void 0 && requireIdentity(backend, config.token, "withConfig()");
|
|
1316
1432
|
const rebinds = config.token !== void 0 || config.accessControl !== void 0;
|
|
1317
|
-
return makeHandle(
|
|
1433
|
+
return makeHandle({
|
|
1318
1434
|
backend,
|
|
1319
|
-
config.projectId ?? projectId,
|
|
1320
|
-
config.dataset ?? dataset,
|
|
1321
|
-
rebinds ? config.accessControl : accessControl,
|
|
1322
|
-
config.perspective ?? perspective,
|
|
1435
|
+
projectId: config.projectId ?? projectId,
|
|
1436
|
+
dataset: config.dataset ?? dataset,
|
|
1437
|
+
accessControl: rebinds ? config.accessControl : accessControl,
|
|
1438
|
+
perspective: config.perspective ?? perspective,
|
|
1323
1439
|
mutationGuard,
|
|
1324
|
-
rebinds ? config.token : token
|
|
1325
|
-
|
|
1440
|
+
token: rebinds ? config.token : token,
|
|
1441
|
+
useProjectHostname: config.useProjectHostname ?? useProjectHostname
|
|
1442
|
+
});
|
|
1326
1443
|
},
|
|
1327
1444
|
seed: (documents) => store.seed(documents),
|
|
1328
1445
|
add: (documents) => store.add(documents),
|