@sanity-labs/client-fake-for-test 0.12.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 +41 -6
- package/dist/index.cjs +596 -56
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +172 -11
- package/dist/index.d.ts +172 -11
- package/dist/index.js +596 -56
- 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,10 +596,13 @@ 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
|
}
|
|
@@ -411,7 +866,8 @@ function releaseNameOf(id) {
|
|
|
411
866
|
return rest.slice(0, dot);
|
|
412
867
|
}
|
|
413
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);
|
|
414
|
-
function applyPerspective(docs,
|
|
869
|
+
function applyPerspective(docs, requested) {
|
|
870
|
+
const perspective = normalizePerspective(requested);
|
|
415
871
|
if (perspective === "raw") return [...docs];
|
|
416
872
|
if (perspective === "published")
|
|
417
873
|
return docs.filter((d) => !isDraftId(d._id) && !isVersionId(d._id) && !isSystemId(d._id));
|
|
@@ -423,6 +879,20 @@ function applyPerspective(docs, perspective) {
|
|
|
423
879
|
entry === "drafts" ? includeDrafts = !0 : stack.push(entry);
|
|
424
880
|
return mergeStack(docs, stack, { includeDrafts, includeReleaseDocs: !0 });
|
|
425
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
|
+
}
|
|
426
896
|
function mergeStack(docs, releaseStack, options) {
|
|
427
897
|
const groups = /* @__PURE__ */ new Map(), systemReleases = [];
|
|
428
898
|
for (const doc of docs) {
|
|
@@ -856,6 +1326,7 @@ const ISSUES_URL = "https://github.com/sanity-labs/client-fake-for-test/issues",
|
|
|
856
1326
|
"createVersion",
|
|
857
1327
|
"unpublishVersion",
|
|
858
1328
|
"request (only with accessControl or a bound token)",
|
|
1329
|
+
"getDataUrl",
|
|
859
1330
|
"config",
|
|
860
1331
|
"withConfig"
|
|
861
1332
|
], UNSUPPORTED_MEMBERS = {
|
|
@@ -875,10 +1346,6 @@ const ISSUES_URL = "https://github.com/sanity-labs/client-fake-for-test/issues",
|
|
|
875
1346
|
what: "build an absolute API URL for a path",
|
|
876
1347
|
workaround: "Hard-code the expected URL in your test, or stub it."
|
|
877
1348
|
},
|
|
878
|
-
getDataUrl: {
|
|
879
|
-
what: "build an absolute data-API URL",
|
|
880
|
-
workaround: "Hard-code the expected URL in your test, or stub it."
|
|
881
|
-
},
|
|
882
1349
|
assets: {
|
|
883
1350
|
what: "asset upload / metadata namespace",
|
|
884
1351
|
workaround: "Seed asset documents into the store directly with `seed()` / `add()`."
|
|
@@ -905,7 +1372,7 @@ const ISSUES_URL = "https://github.com/sanity-labs/client-fake-for-test/issues",
|
|
|
905
1372
|
},
|
|
906
1373
|
request: {
|
|
907
1374
|
what: "generic HTTP request hook \u2014 only wired up when the handle has an acting identity",
|
|
908
|
-
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."
|
|
909
1376
|
},
|
|
910
1377
|
dryRun: {
|
|
911
1378
|
what: "not a method \u2014 did you mean the `{ dryRun }` mutation option?",
|
|
@@ -1063,13 +1530,32 @@ function rejectAmbiguousIdentity(site, config) {
|
|
|
1063
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.`
|
|
1064
1531
|
);
|
|
1065
1532
|
}
|
|
1066
|
-
const registryKey = (
|
|
1533
|
+
const registryKey = (resource) => `${resource.type}:${resource.id}`, datasetResource = (projectId, dataset) => ({
|
|
1534
|
+
type: "dataset",
|
|
1535
|
+
id: `${projectId}.${dataset}`
|
|
1536
|
+
}), parseDatasetKey = (key) => {
|
|
1067
1537
|
const [projectId, dataset] = key.split(":");
|
|
1068
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;
|
|
1069
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
|
+
}
|
|
1070
1555
|
function createTestClient(options = {}) {
|
|
1071
1556
|
const defaultProjectId = options.projectId ?? DEFAULT_PROJECT_ID, defaultDataset = options.dataset ?? DEFAULT_DATASET, backend = {
|
|
1072
1557
|
registry: /* @__PURE__ */ new Map(),
|
|
1558
|
+
organizationId: options.organizationId,
|
|
1073
1559
|
log: [],
|
|
1074
1560
|
stubs: /* @__PURE__ */ new Map(),
|
|
1075
1561
|
calls: [],
|
|
@@ -1077,21 +1563,23 @@ function createTestClient(options = {}) {
|
|
|
1077
1563
|
identities: new Map(Object.entries(options.identities ?? {})),
|
|
1078
1564
|
writeChain: Promise.resolve()
|
|
1079
1565
|
}, registry = backend.registry;
|
|
1080
|
-
rejectAmbiguousIdentity("createTestClient()", options), options.token !== void 0 && requireIdentity(backend, options.token, "createTestClient()")
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
};
|
|
1086
|
-
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)
|
|
1087
1571
|
for (const [key, docs] of Object.entries(options.datasets)) {
|
|
1088
|
-
const { projectId, dataset } =
|
|
1089
|
-
|
|
1572
|
+
const { projectId, dataset } = parseDatasetKey(key);
|
|
1573
|
+
ensureEntry(registry, datasetResource(projectId, dataset)).store.seed(docs);
|
|
1090
1574
|
}
|
|
1575
|
+
if (options.resources)
|
|
1576
|
+
for (const [key, docs] of Object.entries(options.resources))
|
|
1577
|
+
ensureEntry(registry, parseResourceKey(key)).store.seed(docs);
|
|
1091
1578
|
return makeHandle({
|
|
1092
1579
|
backend,
|
|
1093
1580
|
projectId: defaultProjectId,
|
|
1094
1581
|
dataset: defaultDataset,
|
|
1582
|
+
resource: options.resource,
|
|
1095
1583
|
accessControl: options.accessControl,
|
|
1096
1584
|
perspective: options.perspective ?? "raw",
|
|
1097
1585
|
requestTagPrefix: options.requestTagPrefix,
|
|
@@ -1153,39 +1641,52 @@ function toTxnMutations(operations) {
|
|
|
1153
1641
|
function makeHandle(handleArgs) {
|
|
1154
1642
|
const {
|
|
1155
1643
|
backend,
|
|
1156
|
-
|
|
1157
|
-
dataset,
|
|
1644
|
+
resource,
|
|
1158
1645
|
accessControl,
|
|
1159
1646
|
perspective,
|
|
1160
1647
|
requestTagPrefix,
|
|
1161
1648
|
mutationGuard,
|
|
1162
1649
|
token,
|
|
1163
1650
|
useProjectHostname
|
|
1164
|
-
} = handleArgs, { registry } = backend, activeAccess = () => {
|
|
1651
|
+
} = handleArgs, { registry } = backend, { projectId, dataset } = resource?.type === "dataset" ? datasetParts(resource.id) : handleArgs, target = resource ?? datasetResource(projectId, dataset), activeAccess = () => {
|
|
1165
1652
|
if (token === void 0) return accessControl;
|
|
1166
1653
|
const identity = backend.identities.get(token);
|
|
1167
1654
|
if (identity === void 0)
|
|
1168
1655
|
throw fakeError(`no identity is registered for token "${token}" (was it removed?).`);
|
|
1169
1656
|
return {
|
|
1170
|
-
|
|
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,
|
|
1171
1659
|
...identity.globalUser !== void 0 ? { globalUser: identity.globalUser } : {},
|
|
1172
|
-
grants: identity.grants
|
|
1660
|
+
grants: identity.grants,
|
|
1661
|
+
...identity.access !== void 0 ? { access: identity.access } : {}
|
|
1173
1662
|
};
|
|
1174
1663
|
}, requireActiveAccess = () => {
|
|
1175
1664
|
const access = activeAccess();
|
|
1176
1665
|
if (access === void 0)
|
|
1177
1666
|
throw fakeError("this handle has no acting identity (no token or accessControl bound).");
|
|
1178
1667
|
return access;
|
|
1179
|
-
}, store = (() => {
|
|
1180
|
-
const
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
}
|
|
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) => {
|
|
1184
1684
|
const effectiveTag = effectiveRequestTag(tag);
|
|
1185
|
-
backend.log.push({
|
|
1685
|
+
commitHistory(transactionId), backend.log.push({
|
|
1186
1686
|
transactionId,
|
|
1187
1687
|
projectId,
|
|
1188
1688
|
dataset,
|
|
1689
|
+
...resource !== void 0 ? { resource } : {},
|
|
1189
1690
|
mutations,
|
|
1190
1691
|
...effectiveTag !== void 0 ? { tag: effectiveTag } : {}
|
|
1191
1692
|
});
|
|
@@ -1235,10 +1736,10 @@ function makeHandle(handleArgs) {
|
|
|
1235
1736
|
document,
|
|
1236
1737
|
grants: access.grants,
|
|
1237
1738
|
permission: document._id !== void 0 && isSystemId(document._id) ? "manage" : permission,
|
|
1238
|
-
userId:
|
|
1739
|
+
userId: actorId()
|
|
1239
1740
|
});
|
|
1240
1741
|
}, guardGate = async (contexts) => {
|
|
1241
|
-
const identity =
|
|
1742
|
+
const identity = actorId();
|
|
1242
1743
|
await enforceMutationGuards(
|
|
1243
1744
|
mutationGuard,
|
|
1244
1745
|
contexts.map((context) => ({ ...context, identity }))
|
|
@@ -1251,13 +1752,13 @@ function makeHandle(handleArgs) {
|
|
|
1251
1752
|
document,
|
|
1252
1753
|
grants: access.grants,
|
|
1253
1754
|
permission: "read",
|
|
1254
|
-
userId:
|
|
1755
|
+
userId: actorId()
|
|
1255
1756
|
});
|
|
1256
1757
|
}, applyPatch = async (documentId, ops, ifRev, options = {}) => {
|
|
1257
1758
|
const existing = store.get(documentId);
|
|
1258
1759
|
if (existing === void 0)
|
|
1259
1760
|
throw fakeError(
|
|
1260
|
-
`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.`
|
|
1261
1762
|
);
|
|
1262
1763
|
checkRevision(documentId, ifRev), await gate(existing, "update");
|
|
1263
1764
|
const patched = applyPatchOps(existing, ops), now = (/* @__PURE__ */ new Date()).toISOString(), next = {
|
|
@@ -1278,7 +1779,7 @@ function makeHandle(handleArgs) {
|
|
|
1278
1779
|
const promoted = promote2(mut.doc);
|
|
1279
1780
|
if (store.get(promoted._id) !== void 0)
|
|
1280
1781
|
throw fakeError(
|
|
1281
|
-
`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.`
|
|
1282
1783
|
);
|
|
1283
1784
|
await gate(promoted, "create"), store.put(promoted), touchedIds.push(promoted._id), operations.push({ id: promoted._id, operation: "create" });
|
|
1284
1785
|
break;
|
|
@@ -1327,7 +1828,21 @@ function makeHandle(handleArgs) {
|
|
|
1327
1828
|
txnOptions?.returnDocuments ?? !1,
|
|
1328
1829
|
txnOptions?.returnFirst ?? !1
|
|
1329
1830
|
);
|
|
1330
|
-
}, 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 () => {
|
|
1331
1846
|
const all = store.all();
|
|
1332
1847
|
if (activeAccess() === void 0) return all;
|
|
1333
1848
|
const out = [];
|
|
@@ -1371,10 +1886,11 @@ function makeHandle(handleArgs) {
|
|
|
1371
1886
|
const view = await visibleDocs(effective);
|
|
1372
1887
|
return ids.map((id) => resolveDocument(view, id, effective) ?? null);
|
|
1373
1888
|
},
|
|
1374
|
-
|
|
1889
|
+
getDataUrl: (operation, path) => dataUrl({ resource, dataset, operation, path }),
|
|
1890
|
+
create: (doc, opts) => write(async () => {
|
|
1375
1891
|
if (checkRevision(doc._id, opts?.ifRevisionId), store.get(doc._id) !== void 0)
|
|
1376
1892
|
throw fakeError(
|
|
1377
|
-
`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.`
|
|
1378
1894
|
);
|
|
1379
1895
|
const promoted = promote2(doc);
|
|
1380
1896
|
await gate(promoted, "create"), await guardOne("create", null, promoted);
|
|
@@ -1386,7 +1902,7 @@ function makeHandle(handleArgs) {
|
|
|
1386
1902
|
opts
|
|
1387
1903
|
);
|
|
1388
1904
|
}),
|
|
1389
|
-
createOrReplace: (doc, opts) =>
|
|
1905
|
+
createOrReplace: (doc, opts) => write(async () => {
|
|
1390
1906
|
checkRevision(doc._id, opts?.ifRevisionId);
|
|
1391
1907
|
const promoted = promote2(doc), existing = store.get(doc._id);
|
|
1392
1908
|
await gate(existing ?? promoted, existing !== void 0 ? "update" : "create"), await guardOne(existing !== void 0 ? "update" : "create", existing ?? null, promoted);
|
|
@@ -1400,7 +1916,7 @@ function makeHandle(handleArgs) {
|
|
|
1400
1916
|
opts
|
|
1401
1917
|
);
|
|
1402
1918
|
}),
|
|
1403
|
-
createIfNotExists: (doc, opts) =>
|
|
1919
|
+
createIfNotExists: (doc, opts) => write(async () => {
|
|
1404
1920
|
checkRevision(doc._id, opts?.ifRevisionId);
|
|
1405
1921
|
const existing = store.get(doc._id), transactionId = opts?.transactionId ?? cryptoRandomKey();
|
|
1406
1922
|
if (existing !== void 0)
|
|
@@ -1418,7 +1934,7 @@ function makeHandle(handleArgs) {
|
|
|
1418
1934
|
opts
|
|
1419
1935
|
);
|
|
1420
1936
|
}),
|
|
1421
|
-
delete: (id, opts) =>
|
|
1937
|
+
delete: (id, opts) => write(async () => {
|
|
1422
1938
|
checkRevision(id, opts?.ifRevisionId);
|
|
1423
1939
|
const existing = store.get(id);
|
|
1424
1940
|
existing !== void 0 && await gate(existing, "update"), existing !== void 0 && await guardOne("delete", existing, null);
|
|
@@ -1433,7 +1949,7 @@ function makeHandle(handleArgs) {
|
|
|
1433
1949
|
);
|
|
1434
1950
|
}),
|
|
1435
1951
|
patch: (documentId) => createPatch(documentId, {
|
|
1436
|
-
apply: (docId, ops, ifRev, options) =>
|
|
1952
|
+
apply: (docId, ops, ifRev, options) => write(async () => {
|
|
1437
1953
|
const doc = await applyPatch(docId, ops, ifRev);
|
|
1438
1954
|
return record(
|
|
1439
1955
|
cryptoRandomKey(),
|
|
@@ -1450,9 +1966,9 @@ function makeHandle(handleArgs) {
|
|
|
1450
1966
|
})
|
|
1451
1967
|
}),
|
|
1452
1968
|
transaction: () => createTransaction({
|
|
1453
|
-
apply: (mutations, txnOptions) =>
|
|
1969
|
+
apply: (mutations, txnOptions) => write(() => applyTransaction(mutations, txnOptions))
|
|
1454
1970
|
}),
|
|
1455
|
-
mutate: (operations, options) =>
|
|
1971
|
+
mutate: (operations, options) => write(() => applyTransaction(toTxnMutations(operations), options)),
|
|
1456
1972
|
// Synchronous writes still take the lock: an action landing while a
|
|
1457
1973
|
// guarded patch is parked in its gate would otherwise be clobbered
|
|
1458
1974
|
// when that patch resumes over its pre-action snapshot.
|
|
@@ -1501,15 +2017,30 @@ function makeHandle(handleArgs) {
|
|
|
1501
2017
|
...token !== void 0 || accessControl !== void 0 ? {
|
|
1502
2018
|
request: makeRequestImpl({
|
|
1503
2019
|
getAccess: requireActiveAccess,
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
useProjectHostname,
|
|
1507
|
-
|
|
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
|
+
}
|
|
1508
2038
|
})
|
|
1509
2039
|
} : {},
|
|
1510
2040
|
config: () => ({
|
|
1511
2041
|
projectId,
|
|
1512
2042
|
dataset,
|
|
2043
|
+
...resource !== void 0 ? { resource } : {},
|
|
1513
2044
|
perspective,
|
|
1514
2045
|
...requestTagPrefix !== void 0 ? { requestTagPrefix } : {},
|
|
1515
2046
|
...token !== void 0 ? { token } : {},
|
|
@@ -1517,11 +2048,12 @@ function makeHandle(handleArgs) {
|
|
|
1517
2048
|
}),
|
|
1518
2049
|
withConfig: (config) => {
|
|
1519
2050
|
rejectAmbiguousIdentity("withConfig()", config), config.token !== void 0 && requireIdentity(backend, config.token, "withConfig()");
|
|
1520
|
-
const rebinds = config.token !== void 0 || config.accessControl !== void 0;
|
|
1521
|
-
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({
|
|
1522
2053
|
backend,
|
|
1523
2054
|
projectId: config.projectId ?? projectId,
|
|
1524
2055
|
dataset: config.dataset ?? dataset,
|
|
2056
|
+
resource: nextResource,
|
|
1525
2057
|
accessControl: rebinds ? config.accessControl : accessControl,
|
|
1526
2058
|
perspective: config.perspective ?? perspective,
|
|
1527
2059
|
requestTagPrefix: config.requestTagPrefix ?? requestTagPrefix,
|
|
@@ -1539,7 +2071,8 @@ function makeHandle(handleArgs) {
|
|
|
1539
2071
|
const editKey = concurrentEditKey(id);
|
|
1540
2072
|
backend.concurrentEdits.set(editKey, (backend.concurrentEdits.get(editKey) ?? 0) + count);
|
|
1541
2073
|
},
|
|
1542
|
-
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 })),
|
|
1543
2076
|
stub: (name, handler) => {
|
|
1544
2077
|
backend.stubs.set(name, handler);
|
|
1545
2078
|
},
|
|
@@ -1562,7 +2095,13 @@ function makeHandle(handleArgs) {
|
|
|
1562
2095
|
return wrapWithGuards(client, {
|
|
1563
2096
|
getStub: (name) => backend.stubs.get(name),
|
|
1564
2097
|
onCall: (method, args) => {
|
|
1565
|
-
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));
|
|
1566
2105
|
return tag !== void 0 && (rec.tag = tag), backend.calls.push(rec), (outcome) => {
|
|
1567
2106
|
"error" in outcome ? rec.error = outcome.error : rec.result = outcome.result;
|
|
1568
2107
|
};
|
|
@@ -1573,6 +2112,7 @@ function makeHandle(handleArgs) {
|
|
|
1573
2112
|
export {
|
|
1574
2113
|
ERR_PREFIX,
|
|
1575
2114
|
GuardDeniedError,
|
|
2115
|
+
PlatformHttpError,
|
|
1576
2116
|
SUPPORTED_SURFACE,
|
|
1577
2117
|
UNSUPPORTED_MEMBERS,
|
|
1578
2118
|
WriteAccessDeniedError,
|