@sanity-labs/client-fake-for-test 0.11.0 → 0.13.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 +52 -10
- package/dist/index.cjs +672 -78
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +240 -71
- package/dist/index.d.ts +240 -71
- package/dist/index.js +672 -78
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3,6 +3,449 @@ const ERR_PREFIX = "[client-fake-for-test]";
|
|
|
3
3
|
function fakeError(message) {
|
|
4
4
|
return new Error(`${ERR_PREFIX} ${message}`);
|
|
5
5
|
}
|
|
6
|
+
function createDocumentHistory() {
|
|
7
|
+
const transactions = [], revisions = /* @__PURE__ */ new Map(), keep = (doc) => revisions.set(`${doc._id} ${doc._rev}`, structuredClone(doc)), deleted = /* @__PURE__ */ new Map();
|
|
8
|
+
return {
|
|
9
|
+
commit: ({ transactionId, author, before, after }) => {
|
|
10
|
+
const afterById = new Map(after.map((doc) => [doc._id, doc])), ids = /* @__PURE__ */ new Set([...before.keys(), ...afterById.keys()]), effects = [];
|
|
11
|
+
for (const id of ids) {
|
|
12
|
+
const previous = before.get(id), next = afterById.get(id);
|
|
13
|
+
previous !== next && (previous !== void 0 && keep(previous), next !== void 0 && keep(next), next === void 0 && previous !== void 0 ? deleted.set(id, structuredClone(previous)) : deleted.delete(id), effects.push({
|
|
14
|
+
documentId: id,
|
|
15
|
+
after: next === void 0 ? void 0 : structuredClone(next),
|
|
16
|
+
existed: previous !== void 0
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
19
|
+
effects.length !== 0 && transactions.push({
|
|
20
|
+
id: transactionId,
|
|
21
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
22
|
+
author,
|
|
23
|
+
effects
|
|
24
|
+
});
|
|
25
|
+
},
|
|
26
|
+
documentsAt: ({ ids, revision, current }) => ids.flatMap((id) => {
|
|
27
|
+
const image = revisions.get(`${id} ${revision}`) ?? current.find((doc) => doc._id === id && doc._rev === revision);
|
|
28
|
+
return image === void 0 ? [] : [structuredClone(image)];
|
|
29
|
+
}),
|
|
30
|
+
lastImage: (id) => deleted.get(id),
|
|
31
|
+
documentIds: () => [
|
|
32
|
+
...new Set(transactions.flatMap((txn) => txn.effects.map((effect) => effect.documentId)))
|
|
33
|
+
],
|
|
34
|
+
transactions: (ids, visible) => transactions.filter((txn) => txn.effects.some((effect) => ids.includes(effect.documentId))).map((txn) => ({ ...txn, effects: txn.effects.filter((e) => visible(e.documentId)) })).map((txn) => ({
|
|
35
|
+
id: txn.id,
|
|
36
|
+
timestamp: txn.timestamp,
|
|
37
|
+
...txn.author !== void 0 ? { author: txn.author } : {},
|
|
38
|
+
mutations: txn.effects.map((effect) => {
|
|
39
|
+
if (effect.after === void 0) return { delete: { id: effect.documentId } };
|
|
40
|
+
const after = structuredClone(effect.after);
|
|
41
|
+
return effect.existed ? { createOrReplace: after } : { create: after };
|
|
42
|
+
}),
|
|
43
|
+
documentIDs: txn.effects.map((effect) => effect.documentId)
|
|
44
|
+
})),
|
|
45
|
+
events: (ids) => Object.fromEntries(
|
|
46
|
+
ids.map((id) => [
|
|
47
|
+
id,
|
|
48
|
+
transactions.flatMap(
|
|
49
|
+
(txn) => txn.effects.filter((effect) => effect.documentId === id && effect.after !== void 0).map(
|
|
50
|
+
(effect) => ({
|
|
51
|
+
type: effect.existed ? "updateLiveDocument" : "createLiveDocument",
|
|
52
|
+
id: txn.id,
|
|
53
|
+
documentId: id,
|
|
54
|
+
revisionId: effect.after._rev,
|
|
55
|
+
timestamp: txn.timestamp,
|
|
56
|
+
...txn.author !== void 0 ? { author: txn.author } : {}
|
|
57
|
+
})
|
|
58
|
+
)
|
|
59
|
+
).toReversed()
|
|
60
|
+
])
|
|
61
|
+
)
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function historyIds(list) {
|
|
65
|
+
return list.split(",").map((id) => decodeURIComponent(id));
|
|
66
|
+
}
|
|
67
|
+
class PlatformHttpError extends Error {
|
|
68
|
+
statusCode;
|
|
69
|
+
response;
|
|
70
|
+
responseBody;
|
|
71
|
+
details;
|
|
72
|
+
constructor(args) {
|
|
73
|
+
const { method, path, statusCode, statusMessage, body } = args;
|
|
74
|
+
super(clientErrorMessage(args)), this.name = "PlatformHttpError", this.statusCode = statusCode, this.response = { body, url: path, method, headers: {}, statusCode, statusMessage }, this.responseBody = typeof body == "string" ? body : JSON.stringify(body, null, 2);
|
|
75
|
+
const error = isRecord(body) ? body.error : void 0;
|
|
76
|
+
this.details = isRecord(error) ? error : void 0;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function isRecord(value) {
|
|
80
|
+
return typeof value == "object" && value !== null;
|
|
81
|
+
}
|
|
82
|
+
function clientErrorMessage(args) {
|
|
83
|
+
const { body } = args;
|
|
84
|
+
if (isRecord(body)) {
|
|
85
|
+
const { error, message } = body;
|
|
86
|
+
if (isRecord(error) && typeof error.description == "string") return error.description;
|
|
87
|
+
if (typeof message == "string") return message;
|
|
88
|
+
}
|
|
89
|
+
const details = typeof body == "string" ? ` (${body.length > 100 ? `${body.slice(0, 100)}\u2026` : body})` : "";
|
|
90
|
+
return `${args.method}-request to ${args.path} resulted in HTTP ${args.statusCode} ${args.statusMessage}${details}`;
|
|
91
|
+
}
|
|
92
|
+
const notMounted = ({ method, path }) => new PlatformHttpError({
|
|
93
|
+
method,
|
|
94
|
+
path,
|
|
95
|
+
statusCode: 404,
|
|
96
|
+
statusMessage: "Not Found",
|
|
97
|
+
body: `Cannot ${method} ${path}`
|
|
98
|
+
});
|
|
99
|
+
function accessApiError(args) {
|
|
100
|
+
const { request, statusCode, statusMessage, message } = args;
|
|
101
|
+
return new PlatformHttpError({
|
|
102
|
+
...request,
|
|
103
|
+
statusCode,
|
|
104
|
+
statusMessage,
|
|
105
|
+
body: { statusCode, message }
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
const noPermission = (request) => accessApiError({
|
|
109
|
+
request,
|
|
110
|
+
statusCode: 403,
|
|
111
|
+
statusMessage: "Forbidden",
|
|
112
|
+
message: "You do not have permission to access this resource"
|
|
113
|
+
});
|
|
114
|
+
function invalidAccessType(request, type) {
|
|
115
|
+
return accessApiError({
|
|
116
|
+
request,
|
|
117
|
+
statusCode: 400,
|
|
118
|
+
statusMessage: "Bad Request",
|
|
119
|
+
message: `Invalid resource type ${type}`
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
function notFound(name) {
|
|
123
|
+
return (request) => accessApiError({
|
|
124
|
+
request,
|
|
125
|
+
statusCode: 404,
|
|
126
|
+
statusMessage: "Not Found",
|
|
127
|
+
message: `${name} not found`
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
const releaseLimitExceeded = ({ method, path }) => new PlatformHttpError({
|
|
131
|
+
method,
|
|
132
|
+
path,
|
|
133
|
+
statusCode: 403,
|
|
134
|
+
statusMessage: "Forbidden",
|
|
135
|
+
body: {
|
|
136
|
+
error: {
|
|
137
|
+
description: "active release count exceeds the limit of 0",
|
|
138
|
+
type: "releaseLimitExceededError"
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
function serveCapability(capability, request, serve) {
|
|
143
|
+
if (capability !== "served") throw capability(request);
|
|
144
|
+
return serve();
|
|
145
|
+
}
|
|
146
|
+
const ACCESS_KEY_SEPARATOR = ":";
|
|
147
|
+
function accessKey(type, id) {
|
|
148
|
+
return `${type}${ACCESS_KEY_SEPARATOR}${id}`;
|
|
149
|
+
}
|
|
150
|
+
function segmentBase(segment) {
|
|
151
|
+
const pattern = new RegExp(`^/${segment}/([^/]+)(/.*)?$`);
|
|
152
|
+
return (path) => {
|
|
153
|
+
const match = path.match(pattern);
|
|
154
|
+
if (match !== null)
|
|
155
|
+
return { id: decodeURIComponent(match[1]), rest: match[2] ?? "" };
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
const DATASET_BASE = /^\/projects\/([^/]+)\/datasets\/([^/]+)(\/.*)?$/;
|
|
159
|
+
function datasetParts(id) {
|
|
160
|
+
const dot = id.indexOf(".");
|
|
161
|
+
return { projectId: id.slice(0, dot), dataset: id.slice(dot + 1) };
|
|
162
|
+
}
|
|
163
|
+
function accessEntryRoles(type) {
|
|
164
|
+
return ({ identity, id }) => identity.access?.[accessKey(type, id)]?.roles;
|
|
165
|
+
}
|
|
166
|
+
const RESOURCE_CAPABILITIES = {
|
|
167
|
+
dataset: {
|
|
168
|
+
label: (id) => {
|
|
169
|
+
const { projectId, dataset } = datasetParts(id);
|
|
170
|
+
return `dataset "${projectId}:${dataset}"`;
|
|
171
|
+
},
|
|
172
|
+
dataBase: (id) => {
|
|
173
|
+
const { projectId, dataset } = datasetParts(id);
|
|
174
|
+
return `/projects/${projectId}/datasets/${dataset}`;
|
|
175
|
+
},
|
|
176
|
+
matchBase: (path) => {
|
|
177
|
+
const match = path.match(DATASET_BASE);
|
|
178
|
+
if (match !== null)
|
|
179
|
+
return { id: `${match[1]}.${match[2]}`, rest: match[3] ?? "" };
|
|
180
|
+
},
|
|
181
|
+
// A dataset's organization comes from its project: `GET /projects/<projectId>`.
|
|
182
|
+
lookupAtBase: !1,
|
|
183
|
+
identity: "project",
|
|
184
|
+
acl: "served",
|
|
185
|
+
history: "served",
|
|
186
|
+
actions: "served",
|
|
187
|
+
refusedActions: {},
|
|
188
|
+
access: {
|
|
189
|
+
type: "project",
|
|
190
|
+
id: (id) => datasetParts(id).projectId,
|
|
191
|
+
roles: "served",
|
|
192
|
+
users: "served",
|
|
193
|
+
// Every registered identity is a member of every project, with its project roles.
|
|
194
|
+
memberRoles: ({ identity, id }) => (identity.usersByProject?.[id] ?? identity.user).roles ?? []
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
"media-library": {
|
|
198
|
+
label: (id) => `media library "${id}"`,
|
|
199
|
+
dataBase: (id) => `/media-libraries/${id}`,
|
|
200
|
+
matchBase: segmentBase("media-libraries"),
|
|
201
|
+
lookupAtBase: !0,
|
|
202
|
+
identity: "global",
|
|
203
|
+
acl: "served",
|
|
204
|
+
history: "served",
|
|
205
|
+
actions: "served",
|
|
206
|
+
refusedActions: { "sanity.action.release.create": releaseLimitExceeded },
|
|
207
|
+
access: {
|
|
208
|
+
type: "media-library",
|
|
209
|
+
id: (id) => id,
|
|
210
|
+
// Unproven on staging (no token held roles read): refused like a missing permission.
|
|
211
|
+
roles: noPermission,
|
|
212
|
+
users: "served",
|
|
213
|
+
unknownId: notFound("Media library"),
|
|
214
|
+
memberRoles: accessEntryRoles("media-library")
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
canvas: {
|
|
218
|
+
label: (id) => `canvas "${id}"`,
|
|
219
|
+
dataBase: (id) => `/canvases/${id}`,
|
|
220
|
+
matchBase: segmentBase("canvases"),
|
|
221
|
+
lookupAtBase: !0,
|
|
222
|
+
identity: "global",
|
|
223
|
+
acl: "served",
|
|
224
|
+
history: "served",
|
|
225
|
+
actions: notMounted,
|
|
226
|
+
refusedActions: {},
|
|
227
|
+
access: {
|
|
228
|
+
type: "canvas",
|
|
229
|
+
id: (id) => id,
|
|
230
|
+
// Unproven on staging (no canvas roles/members permission exists to grant).
|
|
231
|
+
roles: noPermission,
|
|
232
|
+
users: noPermission,
|
|
233
|
+
unknownId: notFound("Canvas"),
|
|
234
|
+
memberRoles: accessEntryRoles("canvas")
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
dashboard: {
|
|
238
|
+
label: (id) => `dashboard "${id}"`,
|
|
239
|
+
dataBase: (id) => `/dashboards/${id}`,
|
|
240
|
+
matchBase: segmentBase("dashboards"),
|
|
241
|
+
lookupAtBase: !0,
|
|
242
|
+
identity: "global",
|
|
243
|
+
acl: notMounted,
|
|
244
|
+
history: notMounted,
|
|
245
|
+
actions: notMounted,
|
|
246
|
+
refusedActions: {},
|
|
247
|
+
access: {
|
|
248
|
+
type: "dashboard",
|
|
249
|
+
id: (id) => id,
|
|
250
|
+
// Unproven on staging, as for canvas.
|
|
251
|
+
roles: noPermission,
|
|
252
|
+
users: noPermission,
|
|
253
|
+
unknownId: notFound("Dashboard"),
|
|
254
|
+
memberRoles: accessEntryRoles("dashboard")
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
function capabilitiesOf(type) {
|
|
259
|
+
const row = RESOURCE_CAPABILITIES[type];
|
|
260
|
+
if (row === void 0)
|
|
261
|
+
throw fakeError(
|
|
262
|
+
`Unsupported resource type ${JSON.stringify(type)}. The fake serves ${Object.keys(RESOURCE_CAPABILITIES).map((t) => `"${t}"`).join(", ")}.`
|
|
263
|
+
);
|
|
264
|
+
return row;
|
|
265
|
+
}
|
|
266
|
+
function validateResource(resource) {
|
|
267
|
+
const row = capabilitiesOf(resource.type);
|
|
268
|
+
if (resource.type === "dataset" && resource.id.split(".").length !== 2)
|
|
269
|
+
throw fakeError(
|
|
270
|
+
`Dataset resource ID must be in the format "project.dataset" (got ${JSON.stringify(resource.id)}), the same rule @sanity/client applies to \`resource: { type: 'dataset', id }\`.`
|
|
271
|
+
);
|
|
272
|
+
return row;
|
|
273
|
+
}
|
|
274
|
+
function matchResourcePath(path) {
|
|
275
|
+
for (const [type, row] of Object.entries(RESOURCE_CAPABILITIES)) {
|
|
276
|
+
const match = row.matchBase(path);
|
|
277
|
+
if (match !== void 0)
|
|
278
|
+
return { resource: { type, id: match.id }, rest: match.rest };
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
function capabilitiesForAccessType(type) {
|
|
282
|
+
for (const [resourceType, row] of Object.entries(RESOURCE_CAPABILITIES))
|
|
283
|
+
if (row.access.type === type) return { resourceType, row };
|
|
284
|
+
}
|
|
285
|
+
function dataUrl(args) {
|
|
286
|
+
const { resource, dataset, operation, path } = args;
|
|
287
|
+
if (resource !== void 0) {
|
|
288
|
+
const uri2 = path !== void 0 ? `${operation}/${path}` : operation;
|
|
289
|
+
return `${capabilitiesOf(resource.type).dataBase(resource.id)}/${uri2}`.replace(
|
|
290
|
+
/\/($|\?)/,
|
|
291
|
+
"$1"
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
const base = `/${operation}/${dataset}`;
|
|
295
|
+
return `/data${path !== void 0 ? `${base}/${path}` : base}`.replace(/\/($|\?)/, "$1");
|
|
296
|
+
}
|
|
297
|
+
const ACCESS_PATH = /^\/access\/([^/]+)\/([^/]+)(\/.*)?$/, DATA_HISTORY_PATH = /^\/data\/history\/([^/]+)(\/.*)$/;
|
|
298
|
+
function page(data) {
|
|
299
|
+
return { data, nextCursor: null };
|
|
300
|
+
}
|
|
301
|
+
async function servePlatformRequest(ctx, url) {
|
|
302
|
+
const queryStart = url.indexOf("?"), path = queryStart === -1 ? url : url.slice(0, queryStart), query = new URLSearchParams(queryStart === -1 ? "" : url.slice(queryStart + 1)), request = { method: "GET", path }, access = path.match(ACCESS_PATH);
|
|
303
|
+
if (access !== null) {
|
|
304
|
+
const [, type, id, rest2] = access;
|
|
305
|
+
return serveAccess({ ctx, request, type, id: decodeURIComponent(id), rest: rest2 ?? "", query });
|
|
306
|
+
}
|
|
307
|
+
const dataHistory = path.match(DATA_HISTORY_PATH);
|
|
308
|
+
if (dataHistory !== null) {
|
|
309
|
+
const [, dataset, rest2] = dataHistory, resource2 = { type: "dataset", id: `${ctx.projectId}.${dataset}` };
|
|
310
|
+
return serveHistory({ ctx, request, resource: resource2, rest: rest2, query });
|
|
311
|
+
}
|
|
312
|
+
const matched = matchResourcePath(path);
|
|
313
|
+
if (matched === void 0) return;
|
|
314
|
+
const { resource, rest } = matched, row = capabilitiesOf(resource.type);
|
|
315
|
+
if (rest === "") return row.lookupAtBase ? serveLookup(ctx, resource) : void 0;
|
|
316
|
+
if (rest === "/acl")
|
|
317
|
+
return serveCapability(row.acl, request, () => {
|
|
318
|
+
const own = ctx.own(resource);
|
|
319
|
+
return own === void 0 ? void 0 : { value: own.grants };
|
|
320
|
+
});
|
|
321
|
+
if (rest.startsWith("/history/"))
|
|
322
|
+
return serveHistory({ ctx, request, resource, rest: rest.slice(8), query });
|
|
323
|
+
}
|
|
324
|
+
function serveLookup(ctx, resource) {
|
|
325
|
+
if (ctx.organizationId === void 0)
|
|
326
|
+
throw fakeError(
|
|
327
|
+
`request(): GET ${capabilitiesOf(resource.type).dataBase(resource.id)} answers the resource's organization, but no organization is configured. Pass createTestClient({ organizationId }).`
|
|
328
|
+
);
|
|
329
|
+
return { value: { organizationId: ctx.organizationId, id: resource.id, status: "active" } };
|
|
330
|
+
}
|
|
331
|
+
function serveHistory(args) {
|
|
332
|
+
const { ctx, request, resource, rest, query } = args;
|
|
333
|
+
return serveCapability(capabilitiesOf(resource.type).history, request, () => {
|
|
334
|
+
const own = ctx.own(resource);
|
|
335
|
+
return own === void 0 ? void 0 : readHistory({ own, request, rest, query });
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
async function readHistory(args) {
|
|
339
|
+
const { own, request, rest, query } = args, current = new Map(own.documents().map((doc) => [doc._id, doc])), hidden = /* @__PURE__ */ new Set();
|
|
340
|
+
for (const id of /* @__PURE__ */ new Set([...current.keys(), ...own.history.documentIds()])) {
|
|
341
|
+
const image = current.get(id) ?? own.history.lastImage(id);
|
|
342
|
+
image !== void 0 && !await own.canRead(image) && hidden.add(id);
|
|
343
|
+
}
|
|
344
|
+
const visible = (id) => !hidden.has(id), readable = (list) => historyIds(list).filter(visible), documents = rest.match(/^\/documents\/([^/]+)$/);
|
|
345
|
+
if (documents !== null) {
|
|
346
|
+
const revision = query.get("revision");
|
|
347
|
+
if (revision === null)
|
|
348
|
+
throw fakeError(
|
|
349
|
+
`request(): ${request.path} is served for \`?revision=<rev>\` reads only (time-based history reads are not simulated).`
|
|
350
|
+
);
|
|
351
|
+
const ids = readable(documents[1]);
|
|
352
|
+
return {
|
|
353
|
+
value: { documents: own.history.documentsAt({ ids, revision, current: own.documents() }) }
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
const transactions = rest.match(/^\/transactions\/([^/]+)$/);
|
|
357
|
+
if (transactions !== null)
|
|
358
|
+
return { value: own.history.transactions(readable(transactions[1]), visible).map((record) => JSON.stringify(record)).join(`
|
|
359
|
+
`) };
|
|
360
|
+
const events = rest.match(/^\/events\/documents\/([^/]+)$/);
|
|
361
|
+
if (events !== null)
|
|
362
|
+
return {
|
|
363
|
+
value: {
|
|
364
|
+
events: own.history.events(readable(events[1])),
|
|
365
|
+
nextCursor: ""
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
function serveAccess(args) {
|
|
370
|
+
const { ctx, request, type, id, rest, query } = args, found = capabilitiesForAccessType(type);
|
|
371
|
+
if (found === void 0) throw invalidAccessType(request, type);
|
|
372
|
+
const accessType = found.row.access.type, { unknownId } = found.row.access;
|
|
373
|
+
if (unknownId !== void 0 && rest !== "/user-permissions/me/check" && !ctx.exists({ type: found.resourceType, id }))
|
|
374
|
+
throw unknownId(request);
|
|
375
|
+
const permissions = ctx.actorAccess()?.[accessKey(accessType, id)]?.permissions ?? [];
|
|
376
|
+
switch (rest) {
|
|
377
|
+
case "/roles":
|
|
378
|
+
return serveCapability(found.row.access.roles, request, () => ({
|
|
379
|
+
value: page(rolesOn({ row: found.row, id, identities: ctx.identities() }))
|
|
380
|
+
}));
|
|
381
|
+
case "/users":
|
|
382
|
+
return serveCapability(found.row.access.users, request, () => ({
|
|
383
|
+
value: page(usersOn({ row: found.row, id, identities: ctx.identities() }))
|
|
384
|
+
}));
|
|
385
|
+
case "/user-permissions/me":
|
|
386
|
+
return {
|
|
387
|
+
value: page(
|
|
388
|
+
permissions.map((permission) => {
|
|
389
|
+
const dot = permission.lastIndexOf(".");
|
|
390
|
+
return {
|
|
391
|
+
type: permission.slice(0, dot),
|
|
392
|
+
resourceType: accessType,
|
|
393
|
+
resourceId: id,
|
|
394
|
+
action: permission.slice(dot + 1)
|
|
395
|
+
};
|
|
396
|
+
})
|
|
397
|
+
)
|
|
398
|
+
};
|
|
399
|
+
case "/user-permissions/me/check": {
|
|
400
|
+
const requested = query.getAll("permissions").flatMap((value) => value.split(","));
|
|
401
|
+
return {
|
|
402
|
+
value: {
|
|
403
|
+
data: Object.fromEntries(requested.map((p) => [p, permissions.includes(p)]))
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
default:
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
function rolesOn(args) {
|
|
412
|
+
const byName = /* @__PURE__ */ new Map();
|
|
413
|
+
for (const identity of args.identities)
|
|
414
|
+
for (const role of args.row.access.memberRoles({ identity, id: args.id }) ?? [])
|
|
415
|
+
byName.set(role.name, {
|
|
416
|
+
name: role.name,
|
|
417
|
+
...role.title !== void 0 ? { title: role.title } : {},
|
|
418
|
+
...role.description !== void 0 ? { description: role.description } : {}
|
|
419
|
+
});
|
|
420
|
+
return [...byName.values()].toSorted((a, b) => a.name.localeCompare(b.name));
|
|
421
|
+
}
|
|
422
|
+
function usersOn(args) {
|
|
423
|
+
const { row, id, identities } = args, bySanityUserId = /* @__PURE__ */ new Map();
|
|
424
|
+
for (const identity of identities) {
|
|
425
|
+
const roles = row.access.memberRoles({ identity, id });
|
|
426
|
+
if (roles === void 0) continue;
|
|
427
|
+
const user = identity.globalUser ?? identity.user, record = {
|
|
428
|
+
sanityUserId: user.id,
|
|
429
|
+
profile: {
|
|
430
|
+
displayName: user.name ?? user.id,
|
|
431
|
+
...user.email !== void 0 ? { email: user.email } : {}
|
|
432
|
+
},
|
|
433
|
+
memberships: [
|
|
434
|
+
{
|
|
435
|
+
resourceType: row.access.type,
|
|
436
|
+
resourceId: id,
|
|
437
|
+
roleNames: roles.map((role) => role.name).toSorted()
|
|
438
|
+
}
|
|
439
|
+
]
|
|
440
|
+
}, existing = bySanityUserId.get(record.sanityUserId);
|
|
441
|
+
if (existing !== void 0 && JSON.stringify(existing) !== JSON.stringify(record))
|
|
442
|
+
throw fakeError(
|
|
443
|
+
`request(): two registered identities resolve to user "${record.sanityUserId}" on ${row.access.type} "${id}" with different records. Fix the identity registrations so duplicate ids carry identical records.`
|
|
444
|
+
);
|
|
445
|
+
bySanityUserId.set(record.sanityUserId, record);
|
|
446
|
+
}
|
|
447
|
+
return [...bySanityUserId.values()];
|
|
448
|
+
}
|
|
6
449
|
class WriteAccessDeniedError extends Error {
|
|
7
450
|
documentId;
|
|
8
451
|
permission;
|
|
@@ -59,6 +502,12 @@ async function enforceWriteAccess(args) {
|
|
|
59
502
|
function defaultAclPath(projectId, dataset) {
|
|
60
503
|
return `/projects/${projectId}/datasets/${dataset}/acl`;
|
|
61
504
|
}
|
|
505
|
+
function refusedAcl(path) {
|
|
506
|
+
const matched = matchResourcePath(path);
|
|
507
|
+
if (matched?.rest !== "/acl") return;
|
|
508
|
+
const { acl } = capabilitiesOf(matched.resource.type);
|
|
509
|
+
return acl === "served" ? void 0 : acl({ method: "GET", path });
|
|
510
|
+
}
|
|
62
511
|
function projectUserOf(identity, projectId) {
|
|
63
512
|
return identity.usersByProject?.[projectId] ?? identity.user;
|
|
64
513
|
}
|
|
@@ -122,16 +571,19 @@ function serveProject(args) {
|
|
|
122
571
|
...projectUser.id.startsWith("p-") ? { isRobot: !0 } : {},
|
|
123
572
|
...projectUser.roles !== void 0 ? { roles: canonicalProjectRoles(projectUser.roles) } : {}
|
|
124
573
|
};
|
|
125
|
-
});
|
|
126
|
-
return {
|
|
574
|
+
}), members = consistentProjectRecords({ projectId: args.pathProjectId, records });
|
|
575
|
+
return {
|
|
576
|
+
...args.organizationId !== void 0 ? { organizationId: args.organizationId } : {},
|
|
577
|
+
members: [...members.values()]
|
|
578
|
+
};
|
|
127
579
|
}
|
|
128
580
|
function makeRequestImpl(ctx) {
|
|
129
|
-
const { getAccess,
|
|
581
|
+
const { getAccess, useProjectHostname, listIdentities, organizationId, platform } = ctx;
|
|
130
582
|
return async (opts) => {
|
|
131
|
-
const accessControl = getAccess(),
|
|
583
|
+
const accessControl = getAccess(), path = opts.uri ?? opts.url;
|
|
132
584
|
if (path === "/users/me")
|
|
133
585
|
return useProjectHostname ? accessControl.currentUser : accessControl.globalUser ?? accessControl.currentUser;
|
|
134
|
-
if (path === aclPath)
|
|
586
|
+
if (path !== void 0 && path === accessControl.aclPath && refusedAcl(path) === void 0)
|
|
135
587
|
return accessControl.grants;
|
|
136
588
|
const projectUsers = path?.match(PROJECT_USERS_PATH), [, pathProjectId, requestedIds] = projectUsers ?? [];
|
|
137
589
|
if (pathProjectId !== void 0 && requestedIds !== void 0)
|
|
@@ -144,13 +596,58 @@ function makeRequestImpl(ctx) {
|
|
|
144
596
|
if (membersProjectId !== void 0)
|
|
145
597
|
return serveProject({
|
|
146
598
|
pathProjectId: membersProjectId,
|
|
147
|
-
identities: listIdentities()
|
|
599
|
+
identities: listIdentities(),
|
|
600
|
+
organizationId
|
|
148
601
|
});
|
|
602
|
+
const served = path === void 0 ? void 0 : await servePlatformRequest(platform, path);
|
|
603
|
+
if (served !== void 0) return served.value;
|
|
149
604
|
throw fakeError(
|
|
150
|
-
`request(): no handler for path "${path}". With access control configured, the fake serves "/users/me", "
|
|
605
|
+
`request(): no handler for path "${path}". With access control configured, the fake serves "/users/me", the handle's ACL ("<data base>/acl"), "/projects/<projectId>" (members), "/projects/<projectId>/users/<ids>", the History API (client.getDataUrl("history", \u2026)), resource lookup ("/media-libraries/<id>", "/canvases/<id>", "/dashboards/<id>") and the Access API ("/access/<type>/<id>/roles | users | user-permissions/me[/check]"). For any other endpoint, register a stub (client.stub("request", fn)) or intercept the HTTP call with a tool like MSW.`
|
|
151
606
|
);
|
|
152
607
|
};
|
|
153
608
|
}
|
|
609
|
+
class GuardDeniedError extends Error {
|
|
610
|
+
statusCode = 403;
|
|
611
|
+
type = "guardDeniedError";
|
|
612
|
+
denials;
|
|
613
|
+
details;
|
|
614
|
+
response;
|
|
615
|
+
constructor(denials) {
|
|
616
|
+
const unique = uniqueDenials(denials);
|
|
617
|
+
super(`Mutation denied by guard(s) [${unique.map((denial) => denial.guardID).join(", ")}]`), this.name = "GuardDeniedError", this.denials = unique, this.details = { type: this.type, denials: unique }, this.response = {
|
|
618
|
+
body: { error: this.details },
|
|
619
|
+
url: "memory://client-fake-for-test/data/mutate",
|
|
620
|
+
method: "POST",
|
|
621
|
+
headers: {},
|
|
622
|
+
statusCode: this.statusCode,
|
|
623
|
+
statusMessage: "Forbidden"
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
function uniqueDenials(denials) {
|
|
628
|
+
const seen = /* @__PURE__ */ new Set();
|
|
629
|
+
return denials.filter((denial) => seen.has(denial.guardID) ? !1 : (seen.add(denial.guardID), !0));
|
|
630
|
+
}
|
|
631
|
+
function mutationContext(images) {
|
|
632
|
+
const { before, after } = images;
|
|
633
|
+
if (before !== after)
|
|
634
|
+
return before === void 0 ? { action: "create", before: null, after: after ?? null } : after === void 0 ? { action: "delete", before, after: null } : { action: "update", before, after };
|
|
635
|
+
}
|
|
636
|
+
function mutationContexts(images) {
|
|
637
|
+
return [...images].flatMap((image) => {
|
|
638
|
+
const context = mutationContext(image);
|
|
639
|
+
return context === void 0 ? [] : [context];
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
async function enforceMutationGuards(guard, contexts) {
|
|
643
|
+
if (guard === void 0) return;
|
|
644
|
+
const denials = [];
|
|
645
|
+
for (const context of contexts) {
|
|
646
|
+
const denied = await guard(context);
|
|
647
|
+
denied !== void 0 && denials.push(...denied);
|
|
648
|
+
}
|
|
649
|
+
if (denials.length > 0) throw new GuardDeniedError(denials);
|
|
650
|
+
}
|
|
154
651
|
function describeType(value) {
|
|
155
652
|
return value === null ? "null" : Array.isArray(value) ? "an array" : `a ${typeof value}`;
|
|
156
653
|
}
|
|
@@ -369,7 +866,8 @@ function releaseNameOf(id) {
|
|
|
369
866
|
return rest.slice(0, dot);
|
|
370
867
|
}
|
|
371
868
|
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);
|
|
372
|
-
function applyPerspective(docs,
|
|
869
|
+
function applyPerspective(docs, requested) {
|
|
870
|
+
const perspective = normalizePerspective(requested);
|
|
373
871
|
if (perspective === "raw") return [...docs];
|
|
374
872
|
if (perspective === "published")
|
|
375
873
|
return docs.filter((d) => !isDraftId(d._id) && !isVersionId(d._id) && !isSystemId(d._id));
|
|
@@ -381,6 +879,20 @@ function applyPerspective(docs, perspective) {
|
|
|
381
879
|
entry === "drafts" ? includeDrafts = !0 : stack.push(entry);
|
|
382
880
|
return mergeStack(docs, stack, { includeDrafts, includeReleaseDocs: !0 });
|
|
383
881
|
}
|
|
882
|
+
function normalizePerspective(perspective) {
|
|
883
|
+
if (Array.isArray(perspective)) return perspective;
|
|
884
|
+
switch (perspective) {
|
|
885
|
+
case "raw":
|
|
886
|
+
return "raw";
|
|
887
|
+
case "published":
|
|
888
|
+
return "published";
|
|
889
|
+
case "drafts":
|
|
890
|
+
case "previewDrafts":
|
|
891
|
+
return "drafts";
|
|
892
|
+
default:
|
|
893
|
+
return perspective.split(",");
|
|
894
|
+
}
|
|
895
|
+
}
|
|
384
896
|
function mergeStack(docs, releaseStack, options) {
|
|
385
897
|
const groups = /* @__PURE__ */ new Map(), systemReleases = [];
|
|
386
898
|
for (const doc of docs) {
|
|
@@ -684,7 +1196,7 @@ async function applyAction(store, action, hooks) {
|
|
|
684
1196
|
const published = store.get(action.publishedId);
|
|
685
1197
|
requireRevisionMatch(action.draftId, draft._rev, action.ifDraftRevisionId), requireRevisionMatch(action.publishedId, published?._rev, action.ifPublishedRevisionId);
|
|
686
1198
|
const promoted = promote({ ...draft, _id: action.publishedId });
|
|
687
|
-
return published !== void 0 && (promoted._createdAt = published._createdAt), await hooks?.gate?.(promoted, published === void 0 ? "create" : "update"),
|
|
1199
|
+
return published !== void 0 && (promoted._createdAt = published._createdAt), await hooks?.gate?.(promoted, published === void 0 ? "create" : "update"), store.put(promoted), store.remove(action.draftId), { transactionId: cryptoRandomKey() };
|
|
688
1200
|
}
|
|
689
1201
|
case "sanity.action.document.unpublish": {
|
|
690
1202
|
requireDraftOf(action);
|
|
@@ -693,7 +1205,7 @@ async function applyAction(store, action, hooks) {
|
|
|
693
1205
|
throw fakeError(
|
|
694
1206
|
`Cannot unpublish "${action.publishedId}": no published document with that _id exists. document.unpublish retracts a live published document into "${action.draftId}".`
|
|
695
1207
|
);
|
|
696
|
-
if (await hooks?.gate?.(published, "update"),
|
|
1208
|
+
if (await hooks?.gate?.(published, "update"), store.get(action.draftId) === void 0) {
|
|
697
1209
|
const draft = promote({ ...published, _id: action.draftId });
|
|
698
1210
|
draft._createdAt = published._createdAt, store.put(draft);
|
|
699
1211
|
}
|
|
@@ -714,7 +1226,7 @@ function requireRevisionMatch(documentId, actualRev, expectedRev) {
|
|
|
714
1226
|
);
|
|
715
1227
|
}
|
|
716
1228
|
async function applyActions(store, actions, hooks) {
|
|
717
|
-
const list = Array.isArray(actions) ? actions : [actions], snapshot = /* @__PURE__ */ new Map(), remember = (id) => {
|
|
1229
|
+
const list = Array.isArray(actions) ? actions : [actions], snapshot = /* @__PURE__ */ new Map(), guardedIds = /* @__PURE__ */ new Set(), remember = (id) => {
|
|
718
1230
|
snapshot.has(id) || snapshot.set(id, store.get(id));
|
|
719
1231
|
};
|
|
720
1232
|
try {
|
|
@@ -750,13 +1262,18 @@ async function applyActions(store, actions, hooks) {
|
|
|
750
1262
|
remember(a.versionId);
|
|
751
1263
|
break;
|
|
752
1264
|
case "sanity.action.document.publish":
|
|
1265
|
+
remember(a.draftId), remember(a.publishedId), guardedIds.add(a.publishedId), guardedIds.add(a.draftId);
|
|
1266
|
+
break;
|
|
753
1267
|
case "sanity.action.document.unpublish":
|
|
754
|
-
remember(a.draftId), remember(a.publishedId);
|
|
1268
|
+
remember(a.draftId), remember(a.publishedId), guardedIds.add(a.draftId), guardedIds.add(a.publishedId);
|
|
755
1269
|
break;
|
|
756
1270
|
}
|
|
757
1271
|
last = await applyAction(store, a, hooks);
|
|
758
1272
|
}
|
|
759
|
-
|
|
1273
|
+
const contexts = mutationContexts(
|
|
1274
|
+
[...guardedIds].map((id) => ({ before: snapshot.get(id), after: store.get(id) }))
|
|
1275
|
+
);
|
|
1276
|
+
return await hooks?.guard?.(contexts), last;
|
|
760
1277
|
} catch (err) {
|
|
761
1278
|
for (const [id, doc] of snapshot)
|
|
762
1279
|
doc === void 0 ? store.remove(id) : store.put(doc);
|
|
@@ -809,6 +1326,7 @@ const ISSUES_URL = "https://github.com/sanity-labs/client-fake-for-test/issues",
|
|
|
809
1326
|
"createVersion",
|
|
810
1327
|
"unpublishVersion",
|
|
811
1328
|
"request (only with accessControl or a bound token)",
|
|
1329
|
+
"getDataUrl",
|
|
812
1330
|
"config",
|
|
813
1331
|
"withConfig"
|
|
814
1332
|
], UNSUPPORTED_MEMBERS = {
|
|
@@ -828,10 +1346,6 @@ const ISSUES_URL = "https://github.com/sanity-labs/client-fake-for-test/issues",
|
|
|
828
1346
|
what: "build an absolute API URL for a path",
|
|
829
1347
|
workaround: "Hard-code the expected URL in your test, or stub it."
|
|
830
1348
|
},
|
|
831
|
-
getDataUrl: {
|
|
832
|
-
what: "build an absolute data-API URL",
|
|
833
|
-
workaround: "Hard-code the expected URL in your test, or stub it."
|
|
834
|
-
},
|
|
835
1349
|
assets: {
|
|
836
1350
|
what: "asset upload / metadata namespace",
|
|
837
1351
|
workaround: "Seed asset documents into the store directly with `seed()` / `add()`."
|
|
@@ -858,7 +1372,7 @@ const ISSUES_URL = "https://github.com/sanity-labs/client-fake-for-test/issues",
|
|
|
858
1372
|
},
|
|
859
1373
|
request: {
|
|
860
1374
|
what: "generic HTTP request hook \u2014 only wired up when the handle has an acting identity",
|
|
861
|
-
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, /projects/<projectId>,
|
|
1375
|
+
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, /projects/<projectId>, /projects/<projectId>/users/<ids>, the History API, resource lookup and the Access API). Or register a stub."
|
|
862
1376
|
},
|
|
863
1377
|
dryRun: {
|
|
864
1378
|
what: "not a method \u2014 did you mean the `{ dryRun }` mutation option?",
|
|
@@ -1016,13 +1530,32 @@ function rejectAmbiguousIdentity(site, config) {
|
|
|
1016
1530
|
`${site}: \`token\` and \`accessControl\` are mutually exclusive \u2014 a handle acts either as a registered identity (token) or as an inline accessControl config, not both.`
|
|
1017
1531
|
);
|
|
1018
1532
|
}
|
|
1019
|
-
const registryKey = (
|
|
1533
|
+
const registryKey = (resource) => `${resource.type}:${resource.id}`, datasetResource = (projectId, dataset) => ({
|
|
1534
|
+
type: "dataset",
|
|
1535
|
+
id: `${projectId}.${dataset}`
|
|
1536
|
+
}), parseDatasetKey = (key) => {
|
|
1020
1537
|
const [projectId, dataset] = key.split(":");
|
|
1021
1538
|
return { projectId, dataset };
|
|
1539
|
+
}, parseResourceKey = (key) => {
|
|
1540
|
+
const separator = key.indexOf(":"), resource = {
|
|
1541
|
+
type: key.slice(0, separator),
|
|
1542
|
+
id: key.slice(separator + 1)
|
|
1543
|
+
};
|
|
1544
|
+
if (separator <= 0 || resource.id.length === 0)
|
|
1545
|
+
throw fakeError(
|
|
1546
|
+
`createTestClient(): \`resources\` keys are "<type>:<id>" (e.g. "media-library:ml1"); got "${key}".`
|
|
1547
|
+
);
|
|
1548
|
+
return validateResource(resource), resource;
|
|
1022
1549
|
};
|
|
1550
|
+
function ensureEntry(registry, resource) {
|
|
1551
|
+
const key = registryKey(resource);
|
|
1552
|
+
let entry = registry.get(key);
|
|
1553
|
+
return entry === void 0 && (entry = { resource, store: createTestStore(), history: createDocumentHistory() }, registry.set(key, entry)), entry;
|
|
1554
|
+
}
|
|
1023
1555
|
function createTestClient(options = {}) {
|
|
1024
1556
|
const defaultProjectId = options.projectId ?? DEFAULT_PROJECT_ID, defaultDataset = options.dataset ?? DEFAULT_DATASET, backend = {
|
|
1025
1557
|
registry: /* @__PURE__ */ new Map(),
|
|
1558
|
+
organizationId: options.organizationId,
|
|
1026
1559
|
log: [],
|
|
1027
1560
|
stubs: /* @__PURE__ */ new Map(),
|
|
1028
1561
|
calls: [],
|
|
@@ -1030,21 +1563,23 @@ function createTestClient(options = {}) {
|
|
|
1030
1563
|
identities: new Map(Object.entries(options.identities ?? {})),
|
|
1031
1564
|
writeChain: Promise.resolve()
|
|
1032
1565
|
}, registry = backend.registry;
|
|
1033
|
-
rejectAmbiguousIdentity("createTestClient()", options), options.token !== void 0 && requireIdentity(backend, options.token, "createTestClient()")
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
};
|
|
1039
|
-
if (options.documents && ensureStore(defaultProjectId, defaultDataset).seed(options.documents), options.datasets)
|
|
1566
|
+
if (rejectAmbiguousIdentity("createTestClient()", options), options.token !== void 0 && requireIdentity(backend, options.token, "createTestClient()"), options.resource !== void 0 && validateResource(options.resource), options.documents) {
|
|
1567
|
+
const root = options.resource ?? datasetResource(defaultProjectId, defaultDataset);
|
|
1568
|
+
ensureEntry(registry, root).store.seed(options.documents);
|
|
1569
|
+
}
|
|
1570
|
+
if (options.datasets)
|
|
1040
1571
|
for (const [key, docs] of Object.entries(options.datasets)) {
|
|
1041
|
-
const { projectId, dataset } =
|
|
1042
|
-
|
|
1572
|
+
const { projectId, dataset } = parseDatasetKey(key);
|
|
1573
|
+
ensureEntry(registry, datasetResource(projectId, dataset)).store.seed(docs);
|
|
1043
1574
|
}
|
|
1575
|
+
if (options.resources)
|
|
1576
|
+
for (const [key, docs] of Object.entries(options.resources))
|
|
1577
|
+
ensureEntry(registry, parseResourceKey(key)).store.seed(docs);
|
|
1044
1578
|
return makeHandle({
|
|
1045
1579
|
backend,
|
|
1046
1580
|
projectId: defaultProjectId,
|
|
1047
1581
|
dataset: defaultDataset,
|
|
1582
|
+
resource: options.resource,
|
|
1048
1583
|
accessControl: options.accessControl,
|
|
1049
1584
|
perspective: options.perspective ?? "raw",
|
|
1050
1585
|
requestTagPrefix: options.requestTagPrefix,
|
|
@@ -1106,39 +1641,52 @@ function toTxnMutations(operations) {
|
|
|
1106
1641
|
function makeHandle(handleArgs) {
|
|
1107
1642
|
const {
|
|
1108
1643
|
backend,
|
|
1109
|
-
|
|
1110
|
-
dataset,
|
|
1644
|
+
resource,
|
|
1111
1645
|
accessControl,
|
|
1112
1646
|
perspective,
|
|
1113
1647
|
requestTagPrefix,
|
|
1114
1648
|
mutationGuard,
|
|
1115
1649
|
token,
|
|
1116
1650
|
useProjectHostname
|
|
1117
|
-
} = handleArgs, { registry } = backend, activeAccess = () => {
|
|
1651
|
+
} = handleArgs, { registry } = backend, { projectId, dataset } = resource?.type === "dataset" ? datasetParts(resource.id) : handleArgs, target = resource ?? datasetResource(projectId, dataset), activeAccess = () => {
|
|
1118
1652
|
if (token === void 0) return accessControl;
|
|
1119
1653
|
const identity = backend.identities.get(token);
|
|
1120
1654
|
if (identity === void 0)
|
|
1121
1655
|
throw fakeError(`no identity is registered for token "${token}" (was it removed?).`);
|
|
1122
1656
|
return {
|
|
1123
|
-
|
|
1657
|
+
// Per-project records apply only where the resource lives in a project.
|
|
1658
|
+
currentUser: capabilitiesOf(target.type).identity === "project" ? projectUserOf(identity, projectId) : identity.user,
|
|
1124
1659
|
...identity.globalUser !== void 0 ? { globalUser: identity.globalUser } : {},
|
|
1125
|
-
grants: identity.grants
|
|
1660
|
+
grants: identity.grants,
|
|
1661
|
+
...identity.access !== void 0 ? { access: identity.access } : {}
|
|
1126
1662
|
};
|
|
1127
1663
|
}, requireActiveAccess = () => {
|
|
1128
1664
|
const access = activeAccess();
|
|
1129
1665
|
if (access === void 0)
|
|
1130
1666
|
throw fakeError("this handle has no acting identity (no token or accessControl bound).");
|
|
1131
1667
|
return access;
|
|
1132
|
-
}, store = (() => {
|
|
1133
|
-
const
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
}
|
|
1668
|
+
}, capabilities = capabilitiesOf(target.type), targetLabel = capabilities.label(target.id), { store, history } = ensureEntry(registry, target), concurrentEditKey = (id) => registryKey(target) + " " + id, actorId = () => {
|
|
1669
|
+
const access = activeAccess();
|
|
1670
|
+
if (access !== void 0)
|
|
1671
|
+
return capabilities.identity === "global" ? (access.globalUser ?? access.currentUser).id : access.currentUser.id;
|
|
1672
|
+
};
|
|
1673
|
+
let beforeWrite;
|
|
1674
|
+
const write = (run) => serializeWrite(backend, async () => {
|
|
1675
|
+
beforeWrite = new Map(store.all().map((doc) => [doc._id, doc]));
|
|
1676
|
+
try {
|
|
1677
|
+
return await run();
|
|
1678
|
+
} finally {
|
|
1679
|
+
beforeWrite = void 0;
|
|
1680
|
+
}
|
|
1681
|
+
}), commitHistory = (transactionId) => {
|
|
1682
|
+
beforeWrite !== void 0 && history.commit({ transactionId, author: actorId(), before: beforeWrite, after: store.all() });
|
|
1683
|
+
}, record = (transactionId, mutations, tag) => {
|
|
1137
1684
|
const effectiveTag = effectiveRequestTag(tag);
|
|
1138
|
-
backend.log.push({
|
|
1685
|
+
commitHistory(transactionId), backend.log.push({
|
|
1139
1686
|
transactionId,
|
|
1140
1687
|
projectId,
|
|
1141
1688
|
dataset,
|
|
1689
|
+
...resource !== void 0 ? { resource } : {},
|
|
1142
1690
|
mutations,
|
|
1143
1691
|
...effectiveTag !== void 0 ? { tag: effectiveTag } : {}
|
|
1144
1692
|
});
|
|
@@ -1187,24 +1735,30 @@ function makeHandle(handleArgs) {
|
|
|
1187
1735
|
access !== void 0 && await enforceWriteAccess({
|
|
1188
1736
|
document,
|
|
1189
1737
|
grants: access.grants,
|
|
1190
|
-
permission,
|
|
1191
|
-
userId:
|
|
1738
|
+
permission: document._id !== void 0 && isSystemId(document._id) ? "manage" : permission,
|
|
1739
|
+
userId: actorId()
|
|
1192
1740
|
});
|
|
1193
|
-
}, guardGate = async (
|
|
1194
|
-
|
|
1741
|
+
}, guardGate = async (contexts) => {
|
|
1742
|
+
const identity = actorId();
|
|
1743
|
+
await enforceMutationGuards(
|
|
1744
|
+
mutationGuard,
|
|
1745
|
+
contexts.map((context) => ({ ...context, identity }))
|
|
1746
|
+
);
|
|
1747
|
+
}, guardOne = async (action, before, after) => {
|
|
1748
|
+
await guardGate([{ action, before, after }]);
|
|
1195
1749
|
}, canRead = async (document) => {
|
|
1196
1750
|
const access = activeAccess();
|
|
1197
1751
|
return access === void 0 ? !0 : canAccess({
|
|
1198
1752
|
document,
|
|
1199
1753
|
grants: access.grants,
|
|
1200
1754
|
permission: "read",
|
|
1201
|
-
userId:
|
|
1755
|
+
userId: actorId()
|
|
1202
1756
|
});
|
|
1203
|
-
}, applyPatch = async (documentId, ops, ifRev) => {
|
|
1757
|
+
}, applyPatch = async (documentId, ops, ifRev, options = {}) => {
|
|
1204
1758
|
const existing = store.get(documentId);
|
|
1205
1759
|
if (existing === void 0)
|
|
1206
1760
|
throw fakeError(
|
|
1207
|
-
`Cannot patch document "${documentId}": not found \u2014 no document with that _id exists in
|
|
1761
|
+
`Cannot patch document "${documentId}": not found \u2014 no document with that _id exists in ${targetLabel}. patch() requires the target to already exist; use createOrReplace() / createIfNotExists() to upsert, or seed the document first.`
|
|
1208
1762
|
);
|
|
1209
1763
|
checkRevision(documentId, ifRev), await gate(existing, "update");
|
|
1210
1764
|
const patched = applyPatchOps(existing, ops), now = (/* @__PURE__ */ new Date()).toISOString(), next = {
|
|
@@ -1212,7 +1766,7 @@ function makeHandle(handleArgs) {
|
|
|
1212
1766
|
_rev: cryptoRandomKey(),
|
|
1213
1767
|
_updatedAt: now
|
|
1214
1768
|
};
|
|
1215
|
-
return await
|
|
1769
|
+
return options.evaluateGuard !== !1 && await guardOne("update", existing, next), store.put(next), next;
|
|
1216
1770
|
}, applyTransaction = async (mutations, txnOptions) => {
|
|
1217
1771
|
const transactionId = txnOptions?.transactionId ?? cryptoRandomKey(), snapshot = /* @__PURE__ */ new Map(), touchedIds = [], operations = [], remember = (id) => {
|
|
1218
1772
|
snapshot.has(id) || snapshot.set(id, store.get(id));
|
|
@@ -1225,19 +1779,15 @@ function makeHandle(handleArgs) {
|
|
|
1225
1779
|
const promoted = promote2(mut.doc);
|
|
1226
1780
|
if (store.get(promoted._id) !== void 0)
|
|
1227
1781
|
throw fakeError(
|
|
1228
|
-
`create() failed: a document with _id "${promoted._id}" already exists in
|
|
1782
|
+
`create() failed: a document with _id "${promoted._id}" already exists in ${targetLabel}. Use createOrReplace() to overwrite it, or createIfNotExists() to leave the existing one untouched.`
|
|
1229
1783
|
);
|
|
1230
|
-
await gate(promoted, "create"),
|
|
1784
|
+
await gate(promoted, "create"), store.put(promoted), touchedIds.push(promoted._id), operations.push({ id: promoted._id, operation: "create" });
|
|
1231
1785
|
break;
|
|
1232
1786
|
}
|
|
1233
1787
|
case "createOrReplace": {
|
|
1234
1788
|
remember(mut.doc._id);
|
|
1235
1789
|
const promoted = promote2(mut.doc), existing = store.get(promoted._id);
|
|
1236
|
-
await gate(existing ?? promoted, existing !== void 0 ? "update" : "create")
|
|
1237
|
-
existing !== void 0 ? "update" : "create",
|
|
1238
|
-
existing ?? null,
|
|
1239
|
-
promoted
|
|
1240
|
-
);
|
|
1790
|
+
await gate(existing ?? promoted, existing !== void 0 ? "update" : "create");
|
|
1241
1791
|
const wasPresent = existing !== void 0;
|
|
1242
1792
|
store.put(promoted), touchedIds.push(promoted._id), operations.push({ id: promoted._id, operation: wasPresent ? "update" : "create" });
|
|
1243
1793
|
break;
|
|
@@ -1245,21 +1795,25 @@ function makeHandle(handleArgs) {
|
|
|
1245
1795
|
case "createIfNotExists": {
|
|
1246
1796
|
if (remember(mut.doc._id), store.get(mut.doc._id) === void 0) {
|
|
1247
1797
|
const promoted = promote2(mut.doc);
|
|
1248
|
-
await gate(promoted, "create"),
|
|
1798
|
+
await gate(promoted, "create"), store.put(promoted), touchedIds.push(promoted._id), operations.push({ id: promoted._id, operation: "create" });
|
|
1249
1799
|
}
|
|
1250
1800
|
break;
|
|
1251
1801
|
}
|
|
1252
1802
|
case "delete": {
|
|
1253
1803
|
remember(mut.id);
|
|
1254
1804
|
const existing = store.get(mut.id);
|
|
1255
|
-
existing !== void 0 && await gate(existing, "update"),
|
|
1805
|
+
existing !== void 0 && await gate(existing, "update"), store.remove(mut.id) && (touchedIds.push(mut.id), operations.push({ id: mut.id, operation: "delete" }));
|
|
1256
1806
|
break;
|
|
1257
1807
|
}
|
|
1258
1808
|
case "patch": {
|
|
1259
|
-
remember(mut.documentId), await applyPatch(mut.documentId, mut.ops, mut.ifRev), touchedIds.push(mut.documentId), operations.push({ id: mut.documentId, operation: "update" });
|
|
1809
|
+
remember(mut.documentId), await applyPatch(mut.documentId, mut.ops, mut.ifRev, { evaluateGuard: !1 }), touchedIds.push(mut.documentId), operations.push({ id: mut.documentId, operation: "update" });
|
|
1260
1810
|
break;
|
|
1261
1811
|
}
|
|
1262
1812
|
}
|
|
1813
|
+
const contexts = mutationContexts(
|
|
1814
|
+
[...snapshot].map(([id, before]) => ({ before, after: store.get(id) }))
|
|
1815
|
+
);
|
|
1816
|
+
await guardGate(contexts);
|
|
1263
1817
|
} catch (error) {
|
|
1264
1818
|
for (const [id, doc] of snapshot)
|
|
1265
1819
|
doc === void 0 ? store.remove(id) : store.put(doc);
|
|
@@ -1274,7 +1828,21 @@ function makeHandle(handleArgs) {
|
|
|
1274
1828
|
txnOptions?.returnDocuments ?? !1,
|
|
1275
1829
|
txnOptions?.returnFirst ?? !1
|
|
1276
1830
|
);
|
|
1277
|
-
}, runAction =
|
|
1831
|
+
}, runAction = async (actions) => {
|
|
1832
|
+
const request = {
|
|
1833
|
+
method: "POST",
|
|
1834
|
+
path: `${capabilities.dataBase(target.id)}/actions`
|
|
1835
|
+
};
|
|
1836
|
+
if (capabilities.actions !== "served") throw capabilities.actions(request);
|
|
1837
|
+
for (const action of Array.isArray(actions) ? actions : [actions]) {
|
|
1838
|
+
const refused = capabilities.refusedActions[action.actionType];
|
|
1839
|
+
if (refused !== void 0) throw refused(request);
|
|
1840
|
+
}
|
|
1841
|
+
return write(async () => {
|
|
1842
|
+
const result = await applyActions(store, actions, { gate, guard: guardGate });
|
|
1843
|
+
return commitHistory(result.transactionId), result;
|
|
1844
|
+
});
|
|
1845
|
+
}, accessVisibleDocs = async () => {
|
|
1278
1846
|
const all = store.all();
|
|
1279
1847
|
if (activeAccess() === void 0) return all;
|
|
1280
1848
|
const out = [];
|
|
@@ -1318,13 +1886,14 @@ function makeHandle(handleArgs) {
|
|
|
1318
1886
|
const view = await visibleDocs(effective);
|
|
1319
1887
|
return ids.map((id) => resolveDocument(view, id, effective) ?? null);
|
|
1320
1888
|
},
|
|
1321
|
-
|
|
1889
|
+
getDataUrl: (operation, path) => dataUrl({ resource, dataset, operation, path }),
|
|
1890
|
+
create: (doc, opts) => write(async () => {
|
|
1322
1891
|
if (checkRevision(doc._id, opts?.ifRevisionId), store.get(doc._id) !== void 0)
|
|
1323
1892
|
throw fakeError(
|
|
1324
|
-
`create() failed: a document with _id "${doc._id}" already exists in
|
|
1893
|
+
`create() failed: a document with _id "${doc._id}" already exists in ${targetLabel}. Use createOrReplace() to overwrite it, or createIfNotExists() to leave the existing one untouched.`
|
|
1325
1894
|
);
|
|
1326
1895
|
const promoted = promote2(doc);
|
|
1327
|
-
await gate(promoted, "create"), await
|
|
1896
|
+
await gate(promoted, "create"), await guardOne("create", null, promoted);
|
|
1328
1897
|
const transactionId = opts?.transactionId ?? cryptoRandomKey();
|
|
1329
1898
|
return opts?.dryRun !== !0 && (store.put(promoted), record(transactionId, [{ kind: "create", doc }], opts?.tag)), writeResult(
|
|
1330
1899
|
[promoted],
|
|
@@ -1333,10 +1902,10 @@ function makeHandle(handleArgs) {
|
|
|
1333
1902
|
opts
|
|
1334
1903
|
);
|
|
1335
1904
|
}),
|
|
1336
|
-
createOrReplace: (doc, opts) =>
|
|
1905
|
+
createOrReplace: (doc, opts) => write(async () => {
|
|
1337
1906
|
checkRevision(doc._id, opts?.ifRevisionId);
|
|
1338
1907
|
const promoted = promote2(doc), existing = store.get(doc._id);
|
|
1339
|
-
await gate(existing ?? promoted, existing !== void 0 ? "update" : "create"), await
|
|
1908
|
+
await gate(existing ?? promoted, existing !== void 0 ? "update" : "create"), await guardOne(existing !== void 0 ? "update" : "create", existing ?? null, promoted);
|
|
1340
1909
|
const transactionId = opts?.transactionId ?? cryptoRandomKey();
|
|
1341
1910
|
opts?.dryRun !== !0 && (store.put(promoted), record(transactionId, [{ kind: "createOrReplace", doc }], opts?.tag));
|
|
1342
1911
|
const operation = existing !== void 0 ? "update" : "create";
|
|
@@ -1347,7 +1916,7 @@ function makeHandle(handleArgs) {
|
|
|
1347
1916
|
opts
|
|
1348
1917
|
);
|
|
1349
1918
|
}),
|
|
1350
|
-
createIfNotExists: (doc, opts) =>
|
|
1919
|
+
createIfNotExists: (doc, opts) => write(async () => {
|
|
1351
1920
|
checkRevision(doc._id, opts?.ifRevisionId);
|
|
1352
1921
|
const existing = store.get(doc._id), transactionId = opts?.transactionId ?? cryptoRandomKey();
|
|
1353
1922
|
if (existing !== void 0)
|
|
@@ -1358,17 +1927,17 @@ function makeHandle(handleArgs) {
|
|
|
1358
1927
|
opts
|
|
1359
1928
|
);
|
|
1360
1929
|
const promoted = promote2(doc);
|
|
1361
|
-
return await gate(promoted, "create"), await
|
|
1930
|
+
return await gate(promoted, "create"), await guardOne("create", null, promoted), opts?.dryRun !== !0 && (store.put(promoted), record(transactionId, [{ kind: "createIfNotExists", doc }], opts?.tag)), writeResult(
|
|
1362
1931
|
[promoted],
|
|
1363
1932
|
[{ id: promoted._id, operation: "create" }],
|
|
1364
1933
|
transactionId,
|
|
1365
1934
|
opts
|
|
1366
1935
|
);
|
|
1367
1936
|
}),
|
|
1368
|
-
delete: (id, opts) =>
|
|
1937
|
+
delete: (id, opts) => write(async () => {
|
|
1369
1938
|
checkRevision(id, opts?.ifRevisionId);
|
|
1370
1939
|
const existing = store.get(id);
|
|
1371
|
-
existing !== void 0 && await gate(existing, "update"), existing !== void 0 && await
|
|
1940
|
+
existing !== void 0 && await gate(existing, "update"), existing !== void 0 && await guardOne("delete", existing, null);
|
|
1372
1941
|
const transactionId = opts?.transactionId ?? cryptoRandomKey();
|
|
1373
1942
|
let removed = existing;
|
|
1374
1943
|
return opts?.dryRun !== !0 && (removed = store.remove(id), removed !== void 0 && record(transactionId, [{ kind: "delete", id }], opts?.tag)), opts?.returnDocuments === void 0 && opts?.returnFirst === void 0 ? removed !== void 0 ? { _id: id } : null : formatMutationResult(
|
|
@@ -1380,7 +1949,7 @@ function makeHandle(handleArgs) {
|
|
|
1380
1949
|
);
|
|
1381
1950
|
}),
|
|
1382
1951
|
patch: (documentId) => createPatch(documentId, {
|
|
1383
|
-
apply: (docId, ops, ifRev, options) =>
|
|
1952
|
+
apply: (docId, ops, ifRev, options) => write(async () => {
|
|
1384
1953
|
const doc = await applyPatch(docId, ops, ifRev);
|
|
1385
1954
|
return record(
|
|
1386
1955
|
cryptoRandomKey(),
|
|
@@ -1397,9 +1966,9 @@ function makeHandle(handleArgs) {
|
|
|
1397
1966
|
})
|
|
1398
1967
|
}),
|
|
1399
1968
|
transaction: () => createTransaction({
|
|
1400
|
-
apply: (mutations, txnOptions) =>
|
|
1969
|
+
apply: (mutations, txnOptions) => write(() => applyTransaction(mutations, txnOptions))
|
|
1401
1970
|
}),
|
|
1402
|
-
mutate: (operations, options) =>
|
|
1971
|
+
mutate: (operations, options) => write(() => applyTransaction(toTxnMutations(operations), options)),
|
|
1403
1972
|
// Synchronous writes still take the lock: an action landing while a
|
|
1404
1973
|
// guarded patch is parked in its gate would otherwise be clobbered
|
|
1405
1974
|
// when that patch resumes over its pre-action snapshot.
|
|
@@ -1448,15 +2017,30 @@ function makeHandle(handleArgs) {
|
|
|
1448
2017
|
...token !== void 0 || accessControl !== void 0 ? {
|
|
1449
2018
|
request: makeRequestImpl({
|
|
1450
2019
|
getAccess: requireActiveAccess,
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
useProjectHostname,
|
|
1454
|
-
|
|
2020
|
+
// `@sanity/client` sends every request of a `resource`-configured
|
|
2021
|
+
// client to the global API host.
|
|
2022
|
+
useProjectHostname: useProjectHostname && resource === void 0,
|
|
2023
|
+
organizationId: backend.organizationId,
|
|
2024
|
+
listIdentities: () => [...backend.identities.values()],
|
|
2025
|
+
platform: {
|
|
2026
|
+
projectId,
|
|
2027
|
+
organizationId: backend.organizationId,
|
|
2028
|
+
own: (asked) => registryKey(asked) === registryKey(target) ? {
|
|
2029
|
+
grants: requireActiveAccess().grants,
|
|
2030
|
+
documents: () => store.all(),
|
|
2031
|
+
history,
|
|
2032
|
+
canRead
|
|
2033
|
+
} : void 0,
|
|
2034
|
+
actorAccess: () => requireActiveAccess().access,
|
|
2035
|
+
identities: () => [...backend.identities.values()],
|
|
2036
|
+
exists: (asked) => registry.has(registryKey(asked))
|
|
2037
|
+
}
|
|
1455
2038
|
})
|
|
1456
2039
|
} : {},
|
|
1457
2040
|
config: () => ({
|
|
1458
2041
|
projectId,
|
|
1459
2042
|
dataset,
|
|
2043
|
+
...resource !== void 0 ? { resource } : {},
|
|
1460
2044
|
perspective,
|
|
1461
2045
|
...requestTagPrefix !== void 0 ? { requestTagPrefix } : {},
|
|
1462
2046
|
...token !== void 0 ? { token } : {},
|
|
@@ -1464,11 +2048,12 @@ function makeHandle(handleArgs) {
|
|
|
1464
2048
|
}),
|
|
1465
2049
|
withConfig: (config) => {
|
|
1466
2050
|
rejectAmbiguousIdentity("withConfig()", config), config.token !== void 0 && requireIdentity(backend, config.token, "withConfig()");
|
|
1467
|
-
const rebinds = config.token !== void 0 || config.accessControl !== void 0;
|
|
1468
|
-
return makeHandle({
|
|
2051
|
+
const rebinds = config.token !== void 0 || config.accessControl !== void 0, nextResource = "resource" in config ? config.resource : resource;
|
|
2052
|
+
return nextResource !== void 0 && validateResource(nextResource), makeHandle({
|
|
1469
2053
|
backend,
|
|
1470
2054
|
projectId: config.projectId ?? projectId,
|
|
1471
2055
|
dataset: config.dataset ?? dataset,
|
|
2056
|
+
resource: nextResource,
|
|
1472
2057
|
accessControl: rebinds ? config.accessControl : accessControl,
|
|
1473
2058
|
perspective: config.perspective ?? perspective,
|
|
1474
2059
|
requestTagPrefix: config.requestTagPrefix ?? requestTagPrefix,
|
|
@@ -1486,7 +2071,8 @@ function makeHandle(handleArgs) {
|
|
|
1486
2071
|
const editKey = concurrentEditKey(id);
|
|
1487
2072
|
backend.concurrentEdits.set(editKey, (backend.concurrentEdits.get(editKey) ?? 0) + count);
|
|
1488
2073
|
},
|
|
1489
|
-
datasets: () => [...registry.
|
|
2074
|
+
datasets: () => [...registry.values()].filter((entry) => entry.resource.type === "dataset").map((entry) => datasetParts(entry.resource.id)),
|
|
2075
|
+
resources: () => [...registry.values()].map((entry) => ({ ...entry.resource })),
|
|
1490
2076
|
stub: (name, handler) => {
|
|
1491
2077
|
backend.stubs.set(name, handler);
|
|
1492
2078
|
},
|
|
@@ -1509,7 +2095,13 @@ function makeHandle(handleArgs) {
|
|
|
1509
2095
|
return wrapWithGuards(client, {
|
|
1510
2096
|
getStub: (name) => backend.stubs.get(name),
|
|
1511
2097
|
onCall: (method, args) => {
|
|
1512
|
-
const rec = {
|
|
2098
|
+
const rec = {
|
|
2099
|
+
method,
|
|
2100
|
+
args,
|
|
2101
|
+
projectId,
|
|
2102
|
+
dataset,
|
|
2103
|
+
...resource !== void 0 ? { resource } : {}
|
|
2104
|
+
}, tag = OPTIONS_ARG_POSITION[method] === void 0 ? void 0 : effectiveRequestTag(tagFromArgs(method, args));
|
|
1513
2105
|
return tag !== void 0 && (rec.tag = tag), backend.calls.push(rec), (outcome) => {
|
|
1514
2106
|
"error" in outcome ? rec.error = outcome.error : rec.result = outcome.result;
|
|
1515
2107
|
};
|
|
@@ -1519,6 +2111,8 @@ function makeHandle(handleArgs) {
|
|
|
1519
2111
|
}
|
|
1520
2112
|
export {
|
|
1521
2113
|
ERR_PREFIX,
|
|
2114
|
+
GuardDeniedError,
|
|
2115
|
+
PlatformHttpError,
|
|
1522
2116
|
SUPPORTED_SURFACE,
|
|
1523
2117
|
UNSUPPORTED_MEMBERS,
|
|
1524
2118
|
WriteAccessDeniedError,
|