@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.cjs
CHANGED
|
@@ -5,6 +5,449 @@ const ERR_PREFIX = "[client-fake-for-test]";
|
|
|
5
5
|
function fakeError(message) {
|
|
6
6
|
return new Error(`${ERR_PREFIX} ${message}`);
|
|
7
7
|
}
|
|
8
|
+
function createDocumentHistory() {
|
|
9
|
+
const transactions = [], revisions = /* @__PURE__ */ new Map(), keep = (doc) => revisions.set(`${doc._id} ${doc._rev}`, structuredClone(doc)), deleted = /* @__PURE__ */ new Map();
|
|
10
|
+
return {
|
|
11
|
+
commit: ({ transactionId, author, before, after }) => {
|
|
12
|
+
const afterById = new Map(after.map((doc) => [doc._id, doc])), ids = /* @__PURE__ */ new Set([...before.keys(), ...afterById.keys()]), effects = [];
|
|
13
|
+
for (const id of ids) {
|
|
14
|
+
const previous = before.get(id), next = afterById.get(id);
|
|
15
|
+
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({
|
|
16
|
+
documentId: id,
|
|
17
|
+
after: next === void 0 ? void 0 : structuredClone(next),
|
|
18
|
+
existed: previous !== void 0
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
21
|
+
effects.length !== 0 && transactions.push({
|
|
22
|
+
id: transactionId,
|
|
23
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
24
|
+
author,
|
|
25
|
+
effects
|
|
26
|
+
});
|
|
27
|
+
},
|
|
28
|
+
documentsAt: ({ ids, revision, current }) => ids.flatMap((id) => {
|
|
29
|
+
const image = revisions.get(`${id} ${revision}`) ?? current.find((doc) => doc._id === id && doc._rev === revision);
|
|
30
|
+
return image === void 0 ? [] : [structuredClone(image)];
|
|
31
|
+
}),
|
|
32
|
+
lastImage: (id) => deleted.get(id),
|
|
33
|
+
documentIds: () => [
|
|
34
|
+
...new Set(transactions.flatMap((txn) => txn.effects.map((effect) => effect.documentId)))
|
|
35
|
+
],
|
|
36
|
+
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) => ({
|
|
37
|
+
id: txn.id,
|
|
38
|
+
timestamp: txn.timestamp,
|
|
39
|
+
...txn.author !== void 0 ? { author: txn.author } : {},
|
|
40
|
+
mutations: txn.effects.map((effect) => {
|
|
41
|
+
if (effect.after === void 0) return { delete: { id: effect.documentId } };
|
|
42
|
+
const after = structuredClone(effect.after);
|
|
43
|
+
return effect.existed ? { createOrReplace: after } : { create: after };
|
|
44
|
+
}),
|
|
45
|
+
documentIDs: txn.effects.map((effect) => effect.documentId)
|
|
46
|
+
})),
|
|
47
|
+
events: (ids) => Object.fromEntries(
|
|
48
|
+
ids.map((id) => [
|
|
49
|
+
id,
|
|
50
|
+
transactions.flatMap(
|
|
51
|
+
(txn) => txn.effects.filter((effect) => effect.documentId === id && effect.after !== void 0).map(
|
|
52
|
+
(effect) => ({
|
|
53
|
+
type: effect.existed ? "updateLiveDocument" : "createLiveDocument",
|
|
54
|
+
id: txn.id,
|
|
55
|
+
documentId: id,
|
|
56
|
+
revisionId: effect.after._rev,
|
|
57
|
+
timestamp: txn.timestamp,
|
|
58
|
+
...txn.author !== void 0 ? { author: txn.author } : {}
|
|
59
|
+
})
|
|
60
|
+
)
|
|
61
|
+
).toReversed()
|
|
62
|
+
])
|
|
63
|
+
)
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function historyIds(list) {
|
|
67
|
+
return list.split(",").map((id) => decodeURIComponent(id));
|
|
68
|
+
}
|
|
69
|
+
class PlatformHttpError extends Error {
|
|
70
|
+
statusCode;
|
|
71
|
+
response;
|
|
72
|
+
responseBody;
|
|
73
|
+
details;
|
|
74
|
+
constructor(args) {
|
|
75
|
+
const { method, path, statusCode, statusMessage, body } = args;
|
|
76
|
+
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);
|
|
77
|
+
const error = isRecord(body) ? body.error : void 0;
|
|
78
|
+
this.details = isRecord(error) ? error : void 0;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function isRecord(value) {
|
|
82
|
+
return typeof value == "object" && value !== null;
|
|
83
|
+
}
|
|
84
|
+
function clientErrorMessage(args) {
|
|
85
|
+
const { body } = args;
|
|
86
|
+
if (isRecord(body)) {
|
|
87
|
+
const { error, message } = body;
|
|
88
|
+
if (isRecord(error) && typeof error.description == "string") return error.description;
|
|
89
|
+
if (typeof message == "string") return message;
|
|
90
|
+
}
|
|
91
|
+
const details = typeof body == "string" ? ` (${body.length > 100 ? `${body.slice(0, 100)}\u2026` : body})` : "";
|
|
92
|
+
return `${args.method}-request to ${args.path} resulted in HTTP ${args.statusCode} ${args.statusMessage}${details}`;
|
|
93
|
+
}
|
|
94
|
+
const notMounted = ({ method, path }) => new PlatformHttpError({
|
|
95
|
+
method,
|
|
96
|
+
path,
|
|
97
|
+
statusCode: 404,
|
|
98
|
+
statusMessage: "Not Found",
|
|
99
|
+
body: `Cannot ${method} ${path}`
|
|
100
|
+
});
|
|
101
|
+
function accessApiError(args) {
|
|
102
|
+
const { request, statusCode, statusMessage, message } = args;
|
|
103
|
+
return new PlatformHttpError({
|
|
104
|
+
...request,
|
|
105
|
+
statusCode,
|
|
106
|
+
statusMessage,
|
|
107
|
+
body: { statusCode, message }
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
const noPermission = (request) => accessApiError({
|
|
111
|
+
request,
|
|
112
|
+
statusCode: 403,
|
|
113
|
+
statusMessage: "Forbidden",
|
|
114
|
+
message: "You do not have permission to access this resource"
|
|
115
|
+
});
|
|
116
|
+
function invalidAccessType(request, type) {
|
|
117
|
+
return accessApiError({
|
|
118
|
+
request,
|
|
119
|
+
statusCode: 400,
|
|
120
|
+
statusMessage: "Bad Request",
|
|
121
|
+
message: `Invalid resource type ${type}`
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
function notFound(name) {
|
|
125
|
+
return (request) => accessApiError({
|
|
126
|
+
request,
|
|
127
|
+
statusCode: 404,
|
|
128
|
+
statusMessage: "Not Found",
|
|
129
|
+
message: `${name} not found`
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
const releaseLimitExceeded = ({ method, path }) => new PlatformHttpError({
|
|
133
|
+
method,
|
|
134
|
+
path,
|
|
135
|
+
statusCode: 403,
|
|
136
|
+
statusMessage: "Forbidden",
|
|
137
|
+
body: {
|
|
138
|
+
error: {
|
|
139
|
+
description: "active release count exceeds the limit of 0",
|
|
140
|
+
type: "releaseLimitExceededError"
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
function serveCapability(capability, request, serve) {
|
|
145
|
+
if (capability !== "served") throw capability(request);
|
|
146
|
+
return serve();
|
|
147
|
+
}
|
|
148
|
+
const ACCESS_KEY_SEPARATOR = ":";
|
|
149
|
+
function accessKey(type, id) {
|
|
150
|
+
return `${type}${ACCESS_KEY_SEPARATOR}${id}`;
|
|
151
|
+
}
|
|
152
|
+
function segmentBase(segment) {
|
|
153
|
+
const pattern = new RegExp(`^/${segment}/([^/]+)(/.*)?$`);
|
|
154
|
+
return (path) => {
|
|
155
|
+
const match = path.match(pattern);
|
|
156
|
+
if (match !== null)
|
|
157
|
+
return { id: decodeURIComponent(match[1]), rest: match[2] ?? "" };
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
const DATASET_BASE = /^\/projects\/([^/]+)\/datasets\/([^/]+)(\/.*)?$/;
|
|
161
|
+
function datasetParts(id) {
|
|
162
|
+
const dot = id.indexOf(".");
|
|
163
|
+
return { projectId: id.slice(0, dot), dataset: id.slice(dot + 1) };
|
|
164
|
+
}
|
|
165
|
+
function accessEntryRoles(type) {
|
|
166
|
+
return ({ identity, id }) => identity.access?.[accessKey(type, id)]?.roles;
|
|
167
|
+
}
|
|
168
|
+
const RESOURCE_CAPABILITIES = {
|
|
169
|
+
dataset: {
|
|
170
|
+
label: (id) => {
|
|
171
|
+
const { projectId, dataset } = datasetParts(id);
|
|
172
|
+
return `dataset "${projectId}:${dataset}"`;
|
|
173
|
+
},
|
|
174
|
+
dataBase: (id) => {
|
|
175
|
+
const { projectId, dataset } = datasetParts(id);
|
|
176
|
+
return `/projects/${projectId}/datasets/${dataset}`;
|
|
177
|
+
},
|
|
178
|
+
matchBase: (path) => {
|
|
179
|
+
const match = path.match(DATASET_BASE);
|
|
180
|
+
if (match !== null)
|
|
181
|
+
return { id: `${match[1]}.${match[2]}`, rest: match[3] ?? "" };
|
|
182
|
+
},
|
|
183
|
+
// A dataset's organization comes from its project: `GET /projects/<projectId>`.
|
|
184
|
+
lookupAtBase: !1,
|
|
185
|
+
identity: "project",
|
|
186
|
+
acl: "served",
|
|
187
|
+
history: "served",
|
|
188
|
+
actions: "served",
|
|
189
|
+
refusedActions: {},
|
|
190
|
+
access: {
|
|
191
|
+
type: "project",
|
|
192
|
+
id: (id) => datasetParts(id).projectId,
|
|
193
|
+
roles: "served",
|
|
194
|
+
users: "served",
|
|
195
|
+
// Every registered identity is a member of every project, with its project roles.
|
|
196
|
+
memberRoles: ({ identity, id }) => (identity.usersByProject?.[id] ?? identity.user).roles ?? []
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
"media-library": {
|
|
200
|
+
label: (id) => `media library "${id}"`,
|
|
201
|
+
dataBase: (id) => `/media-libraries/${id}`,
|
|
202
|
+
matchBase: segmentBase("media-libraries"),
|
|
203
|
+
lookupAtBase: !0,
|
|
204
|
+
identity: "global",
|
|
205
|
+
acl: "served",
|
|
206
|
+
history: "served",
|
|
207
|
+
actions: "served",
|
|
208
|
+
refusedActions: { "sanity.action.release.create": releaseLimitExceeded },
|
|
209
|
+
access: {
|
|
210
|
+
type: "media-library",
|
|
211
|
+
id: (id) => id,
|
|
212
|
+
// Unproven on staging (no token held roles read): refused like a missing permission.
|
|
213
|
+
roles: noPermission,
|
|
214
|
+
users: "served",
|
|
215
|
+
unknownId: notFound("Media library"),
|
|
216
|
+
memberRoles: accessEntryRoles("media-library")
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
canvas: {
|
|
220
|
+
label: (id) => `canvas "${id}"`,
|
|
221
|
+
dataBase: (id) => `/canvases/${id}`,
|
|
222
|
+
matchBase: segmentBase("canvases"),
|
|
223
|
+
lookupAtBase: !0,
|
|
224
|
+
identity: "global",
|
|
225
|
+
acl: "served",
|
|
226
|
+
history: "served",
|
|
227
|
+
actions: notMounted,
|
|
228
|
+
refusedActions: {},
|
|
229
|
+
access: {
|
|
230
|
+
type: "canvas",
|
|
231
|
+
id: (id) => id,
|
|
232
|
+
// Unproven on staging (no canvas roles/members permission exists to grant).
|
|
233
|
+
roles: noPermission,
|
|
234
|
+
users: noPermission,
|
|
235
|
+
unknownId: notFound("Canvas"),
|
|
236
|
+
memberRoles: accessEntryRoles("canvas")
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
dashboard: {
|
|
240
|
+
label: (id) => `dashboard "${id}"`,
|
|
241
|
+
dataBase: (id) => `/dashboards/${id}`,
|
|
242
|
+
matchBase: segmentBase("dashboards"),
|
|
243
|
+
lookupAtBase: !0,
|
|
244
|
+
identity: "global",
|
|
245
|
+
acl: notMounted,
|
|
246
|
+
history: notMounted,
|
|
247
|
+
actions: notMounted,
|
|
248
|
+
refusedActions: {},
|
|
249
|
+
access: {
|
|
250
|
+
type: "dashboard",
|
|
251
|
+
id: (id) => id,
|
|
252
|
+
// Unproven on staging, as for canvas.
|
|
253
|
+
roles: noPermission,
|
|
254
|
+
users: noPermission,
|
|
255
|
+
unknownId: notFound("Dashboard"),
|
|
256
|
+
memberRoles: accessEntryRoles("dashboard")
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
function capabilitiesOf(type) {
|
|
261
|
+
const row = RESOURCE_CAPABILITIES[type];
|
|
262
|
+
if (row === void 0)
|
|
263
|
+
throw fakeError(
|
|
264
|
+
`Unsupported resource type ${JSON.stringify(type)}. The fake serves ${Object.keys(RESOURCE_CAPABILITIES).map((t) => `"${t}"`).join(", ")}.`
|
|
265
|
+
);
|
|
266
|
+
return row;
|
|
267
|
+
}
|
|
268
|
+
function validateResource(resource) {
|
|
269
|
+
const row = capabilitiesOf(resource.type);
|
|
270
|
+
if (resource.type === "dataset" && resource.id.split(".").length !== 2)
|
|
271
|
+
throw fakeError(
|
|
272
|
+
`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 }\`.`
|
|
273
|
+
);
|
|
274
|
+
return row;
|
|
275
|
+
}
|
|
276
|
+
function matchResourcePath(path) {
|
|
277
|
+
for (const [type, row] of Object.entries(RESOURCE_CAPABILITIES)) {
|
|
278
|
+
const match = row.matchBase(path);
|
|
279
|
+
if (match !== void 0)
|
|
280
|
+
return { resource: { type, id: match.id }, rest: match.rest };
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function capabilitiesForAccessType(type) {
|
|
284
|
+
for (const [resourceType, row] of Object.entries(RESOURCE_CAPABILITIES))
|
|
285
|
+
if (row.access.type === type) return { resourceType, row };
|
|
286
|
+
}
|
|
287
|
+
function dataUrl(args) {
|
|
288
|
+
const { resource, dataset, operation, path } = args;
|
|
289
|
+
if (resource !== void 0) {
|
|
290
|
+
const uri2 = path !== void 0 ? `${operation}/${path}` : operation;
|
|
291
|
+
return `${capabilitiesOf(resource.type).dataBase(resource.id)}/${uri2}`.replace(
|
|
292
|
+
/\/($|\?)/,
|
|
293
|
+
"$1"
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
const base = `/${operation}/${dataset}`;
|
|
297
|
+
return `/data${path !== void 0 ? `${base}/${path}` : base}`.replace(/\/($|\?)/, "$1");
|
|
298
|
+
}
|
|
299
|
+
const ACCESS_PATH = /^\/access\/([^/]+)\/([^/]+)(\/.*)?$/, DATA_HISTORY_PATH = /^\/data\/history\/([^/]+)(\/.*)$/;
|
|
300
|
+
function page(data) {
|
|
301
|
+
return { data, nextCursor: null };
|
|
302
|
+
}
|
|
303
|
+
async function servePlatformRequest(ctx, url) {
|
|
304
|
+
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);
|
|
305
|
+
if (access !== null) {
|
|
306
|
+
const [, type, id, rest2] = access;
|
|
307
|
+
return serveAccess({ ctx, request, type, id: decodeURIComponent(id), rest: rest2 ?? "", query });
|
|
308
|
+
}
|
|
309
|
+
const dataHistory = path.match(DATA_HISTORY_PATH);
|
|
310
|
+
if (dataHistory !== null) {
|
|
311
|
+
const [, dataset, rest2] = dataHistory, resource2 = { type: "dataset", id: `${ctx.projectId}.${dataset}` };
|
|
312
|
+
return serveHistory({ ctx, request, resource: resource2, rest: rest2, query });
|
|
313
|
+
}
|
|
314
|
+
const matched = matchResourcePath(path);
|
|
315
|
+
if (matched === void 0) return;
|
|
316
|
+
const { resource, rest } = matched, row = capabilitiesOf(resource.type);
|
|
317
|
+
if (rest === "") return row.lookupAtBase ? serveLookup(ctx, resource) : void 0;
|
|
318
|
+
if (rest === "/acl")
|
|
319
|
+
return serveCapability(row.acl, request, () => {
|
|
320
|
+
const own = ctx.own(resource);
|
|
321
|
+
return own === void 0 ? void 0 : { value: own.grants };
|
|
322
|
+
});
|
|
323
|
+
if (rest.startsWith("/history/"))
|
|
324
|
+
return serveHistory({ ctx, request, resource, rest: rest.slice(8), query });
|
|
325
|
+
}
|
|
326
|
+
function serveLookup(ctx, resource) {
|
|
327
|
+
if (ctx.organizationId === void 0)
|
|
328
|
+
throw fakeError(
|
|
329
|
+
`request(): GET ${capabilitiesOf(resource.type).dataBase(resource.id)} answers the resource's organization, but no organization is configured. Pass createTestClient({ organizationId }).`
|
|
330
|
+
);
|
|
331
|
+
return { value: { organizationId: ctx.organizationId, id: resource.id, status: "active" } };
|
|
332
|
+
}
|
|
333
|
+
function serveHistory(args) {
|
|
334
|
+
const { ctx, request, resource, rest, query } = args;
|
|
335
|
+
return serveCapability(capabilitiesOf(resource.type).history, request, () => {
|
|
336
|
+
const own = ctx.own(resource);
|
|
337
|
+
return own === void 0 ? void 0 : readHistory({ own, request, rest, query });
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
async function readHistory(args) {
|
|
341
|
+
const { own, request, rest, query } = args, current = new Map(own.documents().map((doc) => [doc._id, doc])), hidden = /* @__PURE__ */ new Set();
|
|
342
|
+
for (const id of /* @__PURE__ */ new Set([...current.keys(), ...own.history.documentIds()])) {
|
|
343
|
+
const image = current.get(id) ?? own.history.lastImage(id);
|
|
344
|
+
image !== void 0 && !await own.canRead(image) && hidden.add(id);
|
|
345
|
+
}
|
|
346
|
+
const visible = (id) => !hidden.has(id), readable = (list) => historyIds(list).filter(visible), documents = rest.match(/^\/documents\/([^/]+)$/);
|
|
347
|
+
if (documents !== null) {
|
|
348
|
+
const revision = query.get("revision");
|
|
349
|
+
if (revision === null)
|
|
350
|
+
throw fakeError(
|
|
351
|
+
`request(): ${request.path} is served for \`?revision=<rev>\` reads only (time-based history reads are not simulated).`
|
|
352
|
+
);
|
|
353
|
+
const ids = readable(documents[1]);
|
|
354
|
+
return {
|
|
355
|
+
value: { documents: own.history.documentsAt({ ids, revision, current: own.documents() }) }
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
const transactions = rest.match(/^\/transactions\/([^/]+)$/);
|
|
359
|
+
if (transactions !== null)
|
|
360
|
+
return { value: own.history.transactions(readable(transactions[1]), visible).map((record) => JSON.stringify(record)).join(`
|
|
361
|
+
`) };
|
|
362
|
+
const events = rest.match(/^\/events\/documents\/([^/]+)$/);
|
|
363
|
+
if (events !== null)
|
|
364
|
+
return {
|
|
365
|
+
value: {
|
|
366
|
+
events: own.history.events(readable(events[1])),
|
|
367
|
+
nextCursor: ""
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
function serveAccess(args) {
|
|
372
|
+
const { ctx, request, type, id, rest, query } = args, found = capabilitiesForAccessType(type);
|
|
373
|
+
if (found === void 0) throw invalidAccessType(request, type);
|
|
374
|
+
const accessType = found.row.access.type, { unknownId } = found.row.access;
|
|
375
|
+
if (unknownId !== void 0 && rest !== "/user-permissions/me/check" && !ctx.exists({ type: found.resourceType, id }))
|
|
376
|
+
throw unknownId(request);
|
|
377
|
+
const permissions = ctx.actorAccess()?.[accessKey(accessType, id)]?.permissions ?? [];
|
|
378
|
+
switch (rest) {
|
|
379
|
+
case "/roles":
|
|
380
|
+
return serveCapability(found.row.access.roles, request, () => ({
|
|
381
|
+
value: page(rolesOn({ row: found.row, id, identities: ctx.identities() }))
|
|
382
|
+
}));
|
|
383
|
+
case "/users":
|
|
384
|
+
return serveCapability(found.row.access.users, request, () => ({
|
|
385
|
+
value: page(usersOn({ row: found.row, id, identities: ctx.identities() }))
|
|
386
|
+
}));
|
|
387
|
+
case "/user-permissions/me":
|
|
388
|
+
return {
|
|
389
|
+
value: page(
|
|
390
|
+
permissions.map((permission) => {
|
|
391
|
+
const dot = permission.lastIndexOf(".");
|
|
392
|
+
return {
|
|
393
|
+
type: permission.slice(0, dot),
|
|
394
|
+
resourceType: accessType,
|
|
395
|
+
resourceId: id,
|
|
396
|
+
action: permission.slice(dot + 1)
|
|
397
|
+
};
|
|
398
|
+
})
|
|
399
|
+
)
|
|
400
|
+
};
|
|
401
|
+
case "/user-permissions/me/check": {
|
|
402
|
+
const requested = query.getAll("permissions").flatMap((value) => value.split(","));
|
|
403
|
+
return {
|
|
404
|
+
value: {
|
|
405
|
+
data: Object.fromEntries(requested.map((p) => [p, permissions.includes(p)]))
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
default:
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
function rolesOn(args) {
|
|
414
|
+
const byName = /* @__PURE__ */ new Map();
|
|
415
|
+
for (const identity of args.identities)
|
|
416
|
+
for (const role of args.row.access.memberRoles({ identity, id: args.id }) ?? [])
|
|
417
|
+
byName.set(role.name, {
|
|
418
|
+
name: role.name,
|
|
419
|
+
...role.title !== void 0 ? { title: role.title } : {},
|
|
420
|
+
...role.description !== void 0 ? { description: role.description } : {}
|
|
421
|
+
});
|
|
422
|
+
return [...byName.values()].toSorted((a, b) => a.name.localeCompare(b.name));
|
|
423
|
+
}
|
|
424
|
+
function usersOn(args) {
|
|
425
|
+
const { row, id, identities } = args, bySanityUserId = /* @__PURE__ */ new Map();
|
|
426
|
+
for (const identity of identities) {
|
|
427
|
+
const roles = row.access.memberRoles({ identity, id });
|
|
428
|
+
if (roles === void 0) continue;
|
|
429
|
+
const user = identity.globalUser ?? identity.user, record = {
|
|
430
|
+
sanityUserId: user.id,
|
|
431
|
+
profile: {
|
|
432
|
+
displayName: user.name ?? user.id,
|
|
433
|
+
...user.email !== void 0 ? { email: user.email } : {}
|
|
434
|
+
},
|
|
435
|
+
memberships: [
|
|
436
|
+
{
|
|
437
|
+
resourceType: row.access.type,
|
|
438
|
+
resourceId: id,
|
|
439
|
+
roleNames: roles.map((role) => role.name).toSorted()
|
|
440
|
+
}
|
|
441
|
+
]
|
|
442
|
+
}, existing = bySanityUserId.get(record.sanityUserId);
|
|
443
|
+
if (existing !== void 0 && JSON.stringify(existing) !== JSON.stringify(record))
|
|
444
|
+
throw fakeError(
|
|
445
|
+
`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.`
|
|
446
|
+
);
|
|
447
|
+
bySanityUserId.set(record.sanityUserId, record);
|
|
448
|
+
}
|
|
449
|
+
return [...bySanityUserId.values()];
|
|
450
|
+
}
|
|
8
451
|
class WriteAccessDeniedError extends Error {
|
|
9
452
|
documentId;
|
|
10
453
|
permission;
|
|
@@ -61,6 +504,12 @@ async function enforceWriteAccess(args) {
|
|
|
61
504
|
function defaultAclPath(projectId, dataset) {
|
|
62
505
|
return `/projects/${projectId}/datasets/${dataset}/acl`;
|
|
63
506
|
}
|
|
507
|
+
function refusedAcl(path) {
|
|
508
|
+
const matched = matchResourcePath(path);
|
|
509
|
+
if (matched?.rest !== "/acl") return;
|
|
510
|
+
const { acl } = capabilitiesOf(matched.resource.type);
|
|
511
|
+
return acl === "served" ? void 0 : acl({ method: "GET", path });
|
|
512
|
+
}
|
|
64
513
|
function projectUserOf(identity, projectId) {
|
|
65
514
|
return identity.usersByProject?.[projectId] ?? identity.user;
|
|
66
515
|
}
|
|
@@ -124,16 +573,19 @@ function serveProject(args) {
|
|
|
124
573
|
...projectUser.id.startsWith("p-") ? { isRobot: !0 } : {},
|
|
125
574
|
...projectUser.roles !== void 0 ? { roles: canonicalProjectRoles(projectUser.roles) } : {}
|
|
126
575
|
};
|
|
127
|
-
});
|
|
128
|
-
return {
|
|
576
|
+
}), members = consistentProjectRecords({ projectId: args.pathProjectId, records });
|
|
577
|
+
return {
|
|
578
|
+
...args.organizationId !== void 0 ? { organizationId: args.organizationId } : {},
|
|
579
|
+
members: [...members.values()]
|
|
580
|
+
};
|
|
129
581
|
}
|
|
130
582
|
function makeRequestImpl(ctx) {
|
|
131
|
-
const { getAccess,
|
|
583
|
+
const { getAccess, useProjectHostname, listIdentities, organizationId, platform } = ctx;
|
|
132
584
|
return async (opts) => {
|
|
133
|
-
const accessControl = getAccess(),
|
|
585
|
+
const accessControl = getAccess(), path = opts.uri ?? opts.url;
|
|
134
586
|
if (path === "/users/me")
|
|
135
587
|
return useProjectHostname ? accessControl.currentUser : accessControl.globalUser ?? accessControl.currentUser;
|
|
136
|
-
if (path === aclPath)
|
|
588
|
+
if (path !== void 0 && path === accessControl.aclPath && refusedAcl(path) === void 0)
|
|
137
589
|
return accessControl.grants;
|
|
138
590
|
const projectUsers = path?.match(PROJECT_USERS_PATH), [, pathProjectId, requestedIds] = projectUsers ?? [];
|
|
139
591
|
if (pathProjectId !== void 0 && requestedIds !== void 0)
|
|
@@ -146,10 +598,13 @@ function makeRequestImpl(ctx) {
|
|
|
146
598
|
if (membersProjectId !== void 0)
|
|
147
599
|
return serveProject({
|
|
148
600
|
pathProjectId: membersProjectId,
|
|
149
|
-
identities: listIdentities()
|
|
601
|
+
identities: listIdentities(),
|
|
602
|
+
organizationId
|
|
150
603
|
});
|
|
604
|
+
const served = path === void 0 ? void 0 : await servePlatformRequest(platform, path);
|
|
605
|
+
if (served !== void 0) return served.value;
|
|
151
606
|
throw fakeError(
|
|
152
|
-
`request(): no handler for path "${path}". With access control configured, the fake serves "/users/me", "
|
|
607
|
+
`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.`
|
|
153
608
|
);
|
|
154
609
|
};
|
|
155
610
|
}
|
|
@@ -413,7 +868,8 @@ function releaseNameOf(id) {
|
|
|
413
868
|
return rest.slice(0, dot);
|
|
414
869
|
}
|
|
415
870
|
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);
|
|
416
|
-
function applyPerspective(docs,
|
|
871
|
+
function applyPerspective(docs, requested) {
|
|
872
|
+
const perspective = normalizePerspective(requested);
|
|
417
873
|
if (perspective === "raw") return [...docs];
|
|
418
874
|
if (perspective === "published")
|
|
419
875
|
return docs.filter((d) => !isDraftId(d._id) && !isVersionId(d._id) && !isSystemId(d._id));
|
|
@@ -425,6 +881,20 @@ function applyPerspective(docs, perspective) {
|
|
|
425
881
|
entry === "drafts" ? includeDrafts = !0 : stack.push(entry);
|
|
426
882
|
return mergeStack(docs, stack, { includeDrafts, includeReleaseDocs: !0 });
|
|
427
883
|
}
|
|
884
|
+
function normalizePerspective(perspective) {
|
|
885
|
+
if (Array.isArray(perspective)) return perspective;
|
|
886
|
+
switch (perspective) {
|
|
887
|
+
case "raw":
|
|
888
|
+
return "raw";
|
|
889
|
+
case "published":
|
|
890
|
+
return "published";
|
|
891
|
+
case "drafts":
|
|
892
|
+
case "previewDrafts":
|
|
893
|
+
return "drafts";
|
|
894
|
+
default:
|
|
895
|
+
return perspective.split(",");
|
|
896
|
+
}
|
|
897
|
+
}
|
|
428
898
|
function mergeStack(docs, releaseStack, options) {
|
|
429
899
|
const groups = /* @__PURE__ */ new Map(), systemReleases = [];
|
|
430
900
|
for (const doc of docs) {
|
|
@@ -858,6 +1328,7 @@ const ISSUES_URL = "https://github.com/sanity-labs/client-fake-for-test/issues",
|
|
|
858
1328
|
"createVersion",
|
|
859
1329
|
"unpublishVersion",
|
|
860
1330
|
"request (only with accessControl or a bound token)",
|
|
1331
|
+
"getDataUrl",
|
|
861
1332
|
"config",
|
|
862
1333
|
"withConfig"
|
|
863
1334
|
], UNSUPPORTED_MEMBERS = {
|
|
@@ -877,10 +1348,6 @@ const ISSUES_URL = "https://github.com/sanity-labs/client-fake-for-test/issues",
|
|
|
877
1348
|
what: "build an absolute API URL for a path",
|
|
878
1349
|
workaround: "Hard-code the expected URL in your test, or stub it."
|
|
879
1350
|
},
|
|
880
|
-
getDataUrl: {
|
|
881
|
-
what: "build an absolute data-API URL",
|
|
882
|
-
workaround: "Hard-code the expected URL in your test, or stub it."
|
|
883
|
-
},
|
|
884
1351
|
assets: {
|
|
885
1352
|
what: "asset upload / metadata namespace",
|
|
886
1353
|
workaround: "Seed asset documents into the store directly with `seed()` / `add()`."
|
|
@@ -907,7 +1374,7 @@ const ISSUES_URL = "https://github.com/sanity-labs/client-fake-for-test/issues",
|
|
|
907
1374
|
},
|
|
908
1375
|
request: {
|
|
909
1376
|
what: "generic HTTP request hook \u2014 only wired up when the handle has an acting identity",
|
|
910
|
-
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>,
|
|
1377
|
+
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."
|
|
911
1378
|
},
|
|
912
1379
|
dryRun: {
|
|
913
1380
|
what: "not a method \u2014 did you mean the `{ dryRun }` mutation option?",
|
|
@@ -1065,13 +1532,32 @@ function rejectAmbiguousIdentity(site, config) {
|
|
|
1065
1532
|
`${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.`
|
|
1066
1533
|
);
|
|
1067
1534
|
}
|
|
1068
|
-
const registryKey = (
|
|
1535
|
+
const registryKey = (resource) => `${resource.type}:${resource.id}`, datasetResource = (projectId, dataset) => ({
|
|
1536
|
+
type: "dataset",
|
|
1537
|
+
id: `${projectId}.${dataset}`
|
|
1538
|
+
}), parseDatasetKey = (key) => {
|
|
1069
1539
|
const [projectId, dataset] = key.split(":");
|
|
1070
1540
|
return { projectId, dataset };
|
|
1541
|
+
}, parseResourceKey = (key) => {
|
|
1542
|
+
const separator = key.indexOf(":"), resource = {
|
|
1543
|
+
type: key.slice(0, separator),
|
|
1544
|
+
id: key.slice(separator + 1)
|
|
1545
|
+
};
|
|
1546
|
+
if (separator <= 0 || resource.id.length === 0)
|
|
1547
|
+
throw fakeError(
|
|
1548
|
+
`createTestClient(): \`resources\` keys are "<type>:<id>" (e.g. "media-library:ml1"); got "${key}".`
|
|
1549
|
+
);
|
|
1550
|
+
return validateResource(resource), resource;
|
|
1071
1551
|
};
|
|
1552
|
+
function ensureEntry(registry, resource) {
|
|
1553
|
+
const key = registryKey(resource);
|
|
1554
|
+
let entry = registry.get(key);
|
|
1555
|
+
return entry === void 0 && (entry = { resource, store: createTestStore(), history: createDocumentHistory() }, registry.set(key, entry)), entry;
|
|
1556
|
+
}
|
|
1072
1557
|
function createTestClient(options = {}) {
|
|
1073
1558
|
const defaultProjectId = options.projectId ?? DEFAULT_PROJECT_ID, defaultDataset = options.dataset ?? DEFAULT_DATASET, backend = {
|
|
1074
1559
|
registry: /* @__PURE__ */ new Map(),
|
|
1560
|
+
organizationId: options.organizationId,
|
|
1075
1561
|
log: [],
|
|
1076
1562
|
stubs: /* @__PURE__ */ new Map(),
|
|
1077
1563
|
calls: [],
|
|
@@ -1079,21 +1565,23 @@ function createTestClient(options = {}) {
|
|
|
1079
1565
|
identities: new Map(Object.entries(options.identities ?? {})),
|
|
1080
1566
|
writeChain: Promise.resolve()
|
|
1081
1567
|
}, registry = backend.registry;
|
|
1082
|
-
rejectAmbiguousIdentity("createTestClient()", options), options.token !== void 0 && requireIdentity(backend, options.token, "createTestClient()")
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
};
|
|
1088
|
-
if (options.documents && ensureStore(defaultProjectId, defaultDataset).seed(options.documents), options.datasets)
|
|
1568
|
+
if (rejectAmbiguousIdentity("createTestClient()", options), options.token !== void 0 && requireIdentity(backend, options.token, "createTestClient()"), options.resource !== void 0 && validateResource(options.resource), options.documents) {
|
|
1569
|
+
const root = options.resource ?? datasetResource(defaultProjectId, defaultDataset);
|
|
1570
|
+
ensureEntry(registry, root).store.seed(options.documents);
|
|
1571
|
+
}
|
|
1572
|
+
if (options.datasets)
|
|
1089
1573
|
for (const [key, docs] of Object.entries(options.datasets)) {
|
|
1090
|
-
const { projectId, dataset } =
|
|
1091
|
-
|
|
1574
|
+
const { projectId, dataset } = parseDatasetKey(key);
|
|
1575
|
+
ensureEntry(registry, datasetResource(projectId, dataset)).store.seed(docs);
|
|
1092
1576
|
}
|
|
1577
|
+
if (options.resources)
|
|
1578
|
+
for (const [key, docs] of Object.entries(options.resources))
|
|
1579
|
+
ensureEntry(registry, parseResourceKey(key)).store.seed(docs);
|
|
1093
1580
|
return makeHandle({
|
|
1094
1581
|
backend,
|
|
1095
1582
|
projectId: defaultProjectId,
|
|
1096
1583
|
dataset: defaultDataset,
|
|
1584
|
+
resource: options.resource,
|
|
1097
1585
|
accessControl: options.accessControl,
|
|
1098
1586
|
perspective: options.perspective ?? "raw",
|
|
1099
1587
|
requestTagPrefix: options.requestTagPrefix,
|
|
@@ -1155,39 +1643,52 @@ function toTxnMutations(operations) {
|
|
|
1155
1643
|
function makeHandle(handleArgs) {
|
|
1156
1644
|
const {
|
|
1157
1645
|
backend,
|
|
1158
|
-
|
|
1159
|
-
dataset,
|
|
1646
|
+
resource,
|
|
1160
1647
|
accessControl,
|
|
1161
1648
|
perspective,
|
|
1162
1649
|
requestTagPrefix,
|
|
1163
1650
|
mutationGuard,
|
|
1164
1651
|
token,
|
|
1165
1652
|
useProjectHostname
|
|
1166
|
-
} = handleArgs, { registry } = backend, activeAccess = () => {
|
|
1653
|
+
} = handleArgs, { registry } = backend, { projectId, dataset } = resource?.type === "dataset" ? datasetParts(resource.id) : handleArgs, target = resource ?? datasetResource(projectId, dataset), activeAccess = () => {
|
|
1167
1654
|
if (token === void 0) return accessControl;
|
|
1168
1655
|
const identity = backend.identities.get(token);
|
|
1169
1656
|
if (identity === void 0)
|
|
1170
1657
|
throw fakeError(`no identity is registered for token "${token}" (was it removed?).`);
|
|
1171
1658
|
return {
|
|
1172
|
-
|
|
1659
|
+
// Per-project records apply only where the resource lives in a project.
|
|
1660
|
+
currentUser: capabilitiesOf(target.type).identity === "project" ? projectUserOf(identity, projectId) : identity.user,
|
|
1173
1661
|
...identity.globalUser !== void 0 ? { globalUser: identity.globalUser } : {},
|
|
1174
|
-
grants: identity.grants
|
|
1662
|
+
grants: identity.grants,
|
|
1663
|
+
...identity.access !== void 0 ? { access: identity.access } : {}
|
|
1175
1664
|
};
|
|
1176
1665
|
}, requireActiveAccess = () => {
|
|
1177
1666
|
const access = activeAccess();
|
|
1178
1667
|
if (access === void 0)
|
|
1179
1668
|
throw fakeError("this handle has no acting identity (no token or accessControl bound).");
|
|
1180
1669
|
return access;
|
|
1181
|
-
}, store = (() => {
|
|
1182
|
-
const
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
}
|
|
1670
|
+
}, capabilities = capabilitiesOf(target.type), targetLabel = capabilities.label(target.id), { store, history } = ensureEntry(registry, target), concurrentEditKey = (id) => registryKey(target) + " " + id, actorId = () => {
|
|
1671
|
+
const access = activeAccess();
|
|
1672
|
+
if (access !== void 0)
|
|
1673
|
+
return capabilities.identity === "global" ? (access.globalUser ?? access.currentUser).id : access.currentUser.id;
|
|
1674
|
+
};
|
|
1675
|
+
let beforeWrite;
|
|
1676
|
+
const write = (run) => serializeWrite(backend, async () => {
|
|
1677
|
+
beforeWrite = new Map(store.all().map((doc) => [doc._id, doc]));
|
|
1678
|
+
try {
|
|
1679
|
+
return await run();
|
|
1680
|
+
} finally {
|
|
1681
|
+
beforeWrite = void 0;
|
|
1682
|
+
}
|
|
1683
|
+
}), commitHistory = (transactionId) => {
|
|
1684
|
+
beforeWrite !== void 0 && history.commit({ transactionId, author: actorId(), before: beforeWrite, after: store.all() });
|
|
1685
|
+
}, record = (transactionId, mutations, tag) => {
|
|
1186
1686
|
const effectiveTag = effectiveRequestTag(tag);
|
|
1187
|
-
backend.log.push({
|
|
1687
|
+
commitHistory(transactionId), backend.log.push({
|
|
1188
1688
|
transactionId,
|
|
1189
1689
|
projectId,
|
|
1190
1690
|
dataset,
|
|
1691
|
+
...resource !== void 0 ? { resource } : {},
|
|
1191
1692
|
mutations,
|
|
1192
1693
|
...effectiveTag !== void 0 ? { tag: effectiveTag } : {}
|
|
1193
1694
|
});
|
|
@@ -1237,10 +1738,10 @@ function makeHandle(handleArgs) {
|
|
|
1237
1738
|
document,
|
|
1238
1739
|
grants: access.grants,
|
|
1239
1740
|
permission: document._id !== void 0 && isSystemId(document._id) ? "manage" : permission,
|
|
1240
|
-
userId:
|
|
1741
|
+
userId: actorId()
|
|
1241
1742
|
});
|
|
1242
1743
|
}, guardGate = async (contexts) => {
|
|
1243
|
-
const identity =
|
|
1744
|
+
const identity = actorId();
|
|
1244
1745
|
await enforceMutationGuards(
|
|
1245
1746
|
mutationGuard,
|
|
1246
1747
|
contexts.map((context) => ({ ...context, identity }))
|
|
@@ -1253,13 +1754,13 @@ function makeHandle(handleArgs) {
|
|
|
1253
1754
|
document,
|
|
1254
1755
|
grants: access.grants,
|
|
1255
1756
|
permission: "read",
|
|
1256
|
-
userId:
|
|
1757
|
+
userId: actorId()
|
|
1257
1758
|
});
|
|
1258
1759
|
}, applyPatch = async (documentId, ops, ifRev, options = {}) => {
|
|
1259
1760
|
const existing = store.get(documentId);
|
|
1260
1761
|
if (existing === void 0)
|
|
1261
1762
|
throw fakeError(
|
|
1262
|
-
`Cannot patch document "${documentId}": not found \u2014 no document with that _id exists in
|
|
1763
|
+
`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.`
|
|
1263
1764
|
);
|
|
1264
1765
|
checkRevision(documentId, ifRev), await gate(existing, "update");
|
|
1265
1766
|
const patched = applyPatchOps(existing, ops), now = (/* @__PURE__ */ new Date()).toISOString(), next = {
|
|
@@ -1280,7 +1781,7 @@ function makeHandle(handleArgs) {
|
|
|
1280
1781
|
const promoted = promote2(mut.doc);
|
|
1281
1782
|
if (store.get(promoted._id) !== void 0)
|
|
1282
1783
|
throw fakeError(
|
|
1283
|
-
`create() failed: a document with _id "${promoted._id}" already exists in
|
|
1784
|
+
`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.`
|
|
1284
1785
|
);
|
|
1285
1786
|
await gate(promoted, "create"), store.put(promoted), touchedIds.push(promoted._id), operations.push({ id: promoted._id, operation: "create" });
|
|
1286
1787
|
break;
|
|
@@ -1329,7 +1830,21 @@ function makeHandle(handleArgs) {
|
|
|
1329
1830
|
txnOptions?.returnDocuments ?? !1,
|
|
1330
1831
|
txnOptions?.returnFirst ?? !1
|
|
1331
1832
|
);
|
|
1332
|
-
}, runAction =
|
|
1833
|
+
}, runAction = async (actions) => {
|
|
1834
|
+
const request = {
|
|
1835
|
+
method: "POST",
|
|
1836
|
+
path: `${capabilities.dataBase(target.id)}/actions`
|
|
1837
|
+
};
|
|
1838
|
+
if (capabilities.actions !== "served") throw capabilities.actions(request);
|
|
1839
|
+
for (const action of Array.isArray(actions) ? actions : [actions]) {
|
|
1840
|
+
const refused = capabilities.refusedActions[action.actionType];
|
|
1841
|
+
if (refused !== void 0) throw refused(request);
|
|
1842
|
+
}
|
|
1843
|
+
return write(async () => {
|
|
1844
|
+
const result = await applyActions(store, actions, { gate, guard: guardGate });
|
|
1845
|
+
return commitHistory(result.transactionId), result;
|
|
1846
|
+
});
|
|
1847
|
+
}, accessVisibleDocs = async () => {
|
|
1333
1848
|
const all = store.all();
|
|
1334
1849
|
if (activeAccess() === void 0) return all;
|
|
1335
1850
|
const out = [];
|
|
@@ -1373,10 +1888,11 @@ function makeHandle(handleArgs) {
|
|
|
1373
1888
|
const view = await visibleDocs(effective);
|
|
1374
1889
|
return ids.map((id) => resolveDocument(view, id, effective) ?? null);
|
|
1375
1890
|
},
|
|
1376
|
-
|
|
1891
|
+
getDataUrl: (operation, path) => dataUrl({ resource, dataset, operation, path }),
|
|
1892
|
+
create: (doc, opts) => write(async () => {
|
|
1377
1893
|
if (checkRevision(doc._id, opts?.ifRevisionId), store.get(doc._id) !== void 0)
|
|
1378
1894
|
throw fakeError(
|
|
1379
|
-
`create() failed: a document with _id "${doc._id}" already exists in
|
|
1895
|
+
`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.`
|
|
1380
1896
|
);
|
|
1381
1897
|
const promoted = promote2(doc);
|
|
1382
1898
|
await gate(promoted, "create"), await guardOne("create", null, promoted);
|
|
@@ -1388,7 +1904,7 @@ function makeHandle(handleArgs) {
|
|
|
1388
1904
|
opts
|
|
1389
1905
|
);
|
|
1390
1906
|
}),
|
|
1391
|
-
createOrReplace: (doc, opts) =>
|
|
1907
|
+
createOrReplace: (doc, opts) => write(async () => {
|
|
1392
1908
|
checkRevision(doc._id, opts?.ifRevisionId);
|
|
1393
1909
|
const promoted = promote2(doc), existing = store.get(doc._id);
|
|
1394
1910
|
await gate(existing ?? promoted, existing !== void 0 ? "update" : "create"), await guardOne(existing !== void 0 ? "update" : "create", existing ?? null, promoted);
|
|
@@ -1402,7 +1918,7 @@ function makeHandle(handleArgs) {
|
|
|
1402
1918
|
opts
|
|
1403
1919
|
);
|
|
1404
1920
|
}),
|
|
1405
|
-
createIfNotExists: (doc, opts) =>
|
|
1921
|
+
createIfNotExists: (doc, opts) => write(async () => {
|
|
1406
1922
|
checkRevision(doc._id, opts?.ifRevisionId);
|
|
1407
1923
|
const existing = store.get(doc._id), transactionId = opts?.transactionId ?? cryptoRandomKey();
|
|
1408
1924
|
if (existing !== void 0)
|
|
@@ -1420,7 +1936,7 @@ function makeHandle(handleArgs) {
|
|
|
1420
1936
|
opts
|
|
1421
1937
|
);
|
|
1422
1938
|
}),
|
|
1423
|
-
delete: (id, opts) =>
|
|
1939
|
+
delete: (id, opts) => write(async () => {
|
|
1424
1940
|
checkRevision(id, opts?.ifRevisionId);
|
|
1425
1941
|
const existing = store.get(id);
|
|
1426
1942
|
existing !== void 0 && await gate(existing, "update"), existing !== void 0 && await guardOne("delete", existing, null);
|
|
@@ -1435,7 +1951,7 @@ function makeHandle(handleArgs) {
|
|
|
1435
1951
|
);
|
|
1436
1952
|
}),
|
|
1437
1953
|
patch: (documentId) => createPatch(documentId, {
|
|
1438
|
-
apply: (docId, ops, ifRev, options) =>
|
|
1954
|
+
apply: (docId, ops, ifRev, options) => write(async () => {
|
|
1439
1955
|
const doc = await applyPatch(docId, ops, ifRev);
|
|
1440
1956
|
return record(
|
|
1441
1957
|
cryptoRandomKey(),
|
|
@@ -1452,9 +1968,9 @@ function makeHandle(handleArgs) {
|
|
|
1452
1968
|
})
|
|
1453
1969
|
}),
|
|
1454
1970
|
transaction: () => createTransaction({
|
|
1455
|
-
apply: (mutations, txnOptions) =>
|
|
1971
|
+
apply: (mutations, txnOptions) => write(() => applyTransaction(mutations, txnOptions))
|
|
1456
1972
|
}),
|
|
1457
|
-
mutate: (operations, options) =>
|
|
1973
|
+
mutate: (operations, options) => write(() => applyTransaction(toTxnMutations(operations), options)),
|
|
1458
1974
|
// Synchronous writes still take the lock: an action landing while a
|
|
1459
1975
|
// guarded patch is parked in its gate would otherwise be clobbered
|
|
1460
1976
|
// when that patch resumes over its pre-action snapshot.
|
|
@@ -1503,15 +2019,30 @@ function makeHandle(handleArgs) {
|
|
|
1503
2019
|
...token !== void 0 || accessControl !== void 0 ? {
|
|
1504
2020
|
request: makeRequestImpl({
|
|
1505
2021
|
getAccess: requireActiveAccess,
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
useProjectHostname,
|
|
1509
|
-
|
|
2022
|
+
// `@sanity/client` sends every request of a `resource`-configured
|
|
2023
|
+
// client to the global API host.
|
|
2024
|
+
useProjectHostname: useProjectHostname && resource === void 0,
|
|
2025
|
+
organizationId: backend.organizationId,
|
|
2026
|
+
listIdentities: () => [...backend.identities.values()],
|
|
2027
|
+
platform: {
|
|
2028
|
+
projectId,
|
|
2029
|
+
organizationId: backend.organizationId,
|
|
2030
|
+
own: (asked) => registryKey(asked) === registryKey(target) ? {
|
|
2031
|
+
grants: requireActiveAccess().grants,
|
|
2032
|
+
documents: () => store.all(),
|
|
2033
|
+
history,
|
|
2034
|
+
canRead
|
|
2035
|
+
} : void 0,
|
|
2036
|
+
actorAccess: () => requireActiveAccess().access,
|
|
2037
|
+
identities: () => [...backend.identities.values()],
|
|
2038
|
+
exists: (asked) => registry.has(registryKey(asked))
|
|
2039
|
+
}
|
|
1510
2040
|
})
|
|
1511
2041
|
} : {},
|
|
1512
2042
|
config: () => ({
|
|
1513
2043
|
projectId,
|
|
1514
2044
|
dataset,
|
|
2045
|
+
...resource !== void 0 ? { resource } : {},
|
|
1515
2046
|
perspective,
|
|
1516
2047
|
...requestTagPrefix !== void 0 ? { requestTagPrefix } : {},
|
|
1517
2048
|
...token !== void 0 ? { token } : {},
|
|
@@ -1519,11 +2050,12 @@ function makeHandle(handleArgs) {
|
|
|
1519
2050
|
}),
|
|
1520
2051
|
withConfig: (config) => {
|
|
1521
2052
|
rejectAmbiguousIdentity("withConfig()", config), config.token !== void 0 && requireIdentity(backend, config.token, "withConfig()");
|
|
1522
|
-
const rebinds = config.token !== void 0 || config.accessControl !== void 0;
|
|
1523
|
-
return makeHandle({
|
|
2053
|
+
const rebinds = config.token !== void 0 || config.accessControl !== void 0, nextResource = "resource" in config ? config.resource : resource;
|
|
2054
|
+
return nextResource !== void 0 && validateResource(nextResource), makeHandle({
|
|
1524
2055
|
backend,
|
|
1525
2056
|
projectId: config.projectId ?? projectId,
|
|
1526
2057
|
dataset: config.dataset ?? dataset,
|
|
2058
|
+
resource: nextResource,
|
|
1527
2059
|
accessControl: rebinds ? config.accessControl : accessControl,
|
|
1528
2060
|
perspective: config.perspective ?? perspective,
|
|
1529
2061
|
requestTagPrefix: config.requestTagPrefix ?? requestTagPrefix,
|
|
@@ -1541,7 +2073,8 @@ function makeHandle(handleArgs) {
|
|
|
1541
2073
|
const editKey = concurrentEditKey(id);
|
|
1542
2074
|
backend.concurrentEdits.set(editKey, (backend.concurrentEdits.get(editKey) ?? 0) + count);
|
|
1543
2075
|
},
|
|
1544
|
-
datasets: () => [...registry.
|
|
2076
|
+
datasets: () => [...registry.values()].filter((entry) => entry.resource.type === "dataset").map((entry) => datasetParts(entry.resource.id)),
|
|
2077
|
+
resources: () => [...registry.values()].map((entry) => ({ ...entry.resource })),
|
|
1545
2078
|
stub: (name, handler) => {
|
|
1546
2079
|
backend.stubs.set(name, handler);
|
|
1547
2080
|
},
|
|
@@ -1564,7 +2097,13 @@ function makeHandle(handleArgs) {
|
|
|
1564
2097
|
return wrapWithGuards(client, {
|
|
1565
2098
|
getStub: (name) => backend.stubs.get(name),
|
|
1566
2099
|
onCall: (method, args) => {
|
|
1567
|
-
const rec = {
|
|
2100
|
+
const rec = {
|
|
2101
|
+
method,
|
|
2102
|
+
args,
|
|
2103
|
+
projectId,
|
|
2104
|
+
dataset,
|
|
2105
|
+
...resource !== void 0 ? { resource } : {}
|
|
2106
|
+
}, tag = OPTIONS_ARG_POSITION[method] === void 0 ? void 0 : effectiveRequestTag(tagFromArgs(method, args));
|
|
1568
2107
|
return tag !== void 0 && (rec.tag = tag), backend.calls.push(rec), (outcome) => {
|
|
1569
2108
|
"error" in outcome ? rec.error = outcome.error : rec.result = outcome.result;
|
|
1570
2109
|
};
|
|
@@ -1574,6 +2113,7 @@ function makeHandle(handleArgs) {
|
|
|
1574
2113
|
}
|
|
1575
2114
|
exports.ERR_PREFIX = ERR_PREFIX;
|
|
1576
2115
|
exports.GuardDeniedError = GuardDeniedError;
|
|
2116
|
+
exports.PlatformHttpError = PlatformHttpError;
|
|
1577
2117
|
exports.SUPPORTED_SURFACE = SUPPORTED_SURFACE;
|
|
1578
2118
|
exports.UNSUPPORTED_MEMBERS = UNSUPPORTED_MEMBERS;
|
|
1579
2119
|
exports.WriteAccessDeniedError = WriteAccessDeniedError;
|