@riducms/sdk 0.1.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/dist/client.js ADDED
@@ -0,0 +1,862 @@
1
+ import { isPageEnvelope, } from "@riducms/protocol";
2
+ import { RiduError } from "./error.js";
3
+ export function createClient(options) {
4
+ return new FetchClient(options);
5
+ }
6
+ class FetchClient {
7
+ #baseURL;
8
+ #fetch;
9
+ #headers;
10
+ #credentials;
11
+ #dispatch;
12
+ constructor(options) {
13
+ const baseURL = new URL(options.baseURL);
14
+ this.#baseURL = baseURL.href.replace(/\/$/, "");
15
+ this.#fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
16
+ this.#headers = options.headers;
17
+ this.#credentials = options.credentials ?? "include";
18
+ this.#dispatch = composeMiddleware(options.middleware ?? [], (request) => this.#fetch(request));
19
+ }
20
+ async schema(options) {
21
+ const body = await this.#request("/api/schema", { method: "GET" }, options);
22
+ if (!isRecord(body) || !isRecord(body.schema) || !Array.isArray(body.schema.collections)) {
23
+ throw invalidSuccessEnvelope("schema");
24
+ }
25
+ return body.schema;
26
+ }
27
+ async request(path, init = {}, options) {
28
+ return this.#response(path, init, options, false);
29
+ }
30
+ async requestPlugin(plugin, path, body, options) {
31
+ if (!/^[a-z][a-z0-9-]*$/.test(plugin))
32
+ throw new TypeError("invalid plugin key");
33
+ const segments = path.split("/");
34
+ if (path.length === 0 ||
35
+ path.trim() !== path ||
36
+ /[\s?#*]/u.test(path) ||
37
+ segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
38
+ throw new TypeError("invalid plugin endpoint path");
39
+ }
40
+ const encodedPath = segments.map((segment) => encodeURIComponent(segment)).join("/");
41
+ return (await this.#request(`/api/plugins/${encodeURIComponent(plugin)}/${encodedPath}`, { method: "POST", body: JSON.stringify(body) }, options));
42
+ }
43
+ async preference(key, options) {
44
+ const body = await this.#request(`/api/preferences/${encodeURIComponent(key)}`, { method: "GET" }, options);
45
+ if (!isRecord(body) || !("value" in body))
46
+ throw invalidSuccessEnvelope("preference");
47
+ return body.value;
48
+ }
49
+ async setPreference(key, value, options) {
50
+ const body = await this.#request(`/api/preferences/${encodeURIComponent(key)}`, { method: "PUT", body: JSON.stringify({ value }) }, options);
51
+ if (!isRecord(body) || !("value" in body))
52
+ throw invalidSuccessEnvelope("preference");
53
+ return body.value;
54
+ }
55
+ async deletePreference(key, options) {
56
+ const body = await this.#request(`/api/preferences/${encodeURIComponent(key)}`, { method: "DELETE" }, options);
57
+ if (!isRecord(body) || body.deleted !== true || typeof body.id !== "string") {
58
+ throw invalidSuccessEnvelope("delete preference");
59
+ }
60
+ return { id: body.id, deleted: true };
61
+ }
62
+ async resetPreferences(options) {
63
+ const body = await this.#request("/api/preferences", { method: "DELETE" }, options);
64
+ if (!isRecord(body) || body.success !== true) {
65
+ throw invalidSuccessEnvelope("preference reset");
66
+ }
67
+ return { success: true };
68
+ }
69
+ async collectionAccess(collection, options) {
70
+ const query = new URLSearchParams();
71
+ appendLocaleQuery(query, options);
72
+ const suffix = query.size === 0 ? "" : `?${query}`;
73
+ const body = await this.#request(`/api/access/collections/${encodeURIComponent(collection)}${suffix}`, {
74
+ method: "POST",
75
+ body: JSON.stringify({
76
+ ...(options?.id === undefined ? {} : { id: options.id }),
77
+ ...(options?.data === undefined ? {} : { data: options.data }),
78
+ ...(options?.trash === true ? { trash: true } : {}),
79
+ }),
80
+ }, options);
81
+ return accessCapabilitiesFromEnvelope(body);
82
+ }
83
+ async resolveFilteredSelection(collection, options) {
84
+ const query = new URLSearchParams();
85
+ appendLocaleQuery(query, options);
86
+ const suffix = query.size === 0 ? "" : `?${query}`;
87
+ const body = await this.#request(`/api/access/collections/${encodeURIComponent(collection)}/selection${suffix}`, {
88
+ method: "POST",
89
+ body: JSON.stringify({
90
+ ...(options?.where === undefined ? {} : { where: options.where }),
91
+ ...(options?.trash === true ? { trash: true } : {}),
92
+ }),
93
+ }, options);
94
+ return collectionSelectionFromEnvelope(body);
95
+ }
96
+ async globalAccess(slug, options) {
97
+ const query = new URLSearchParams();
98
+ appendLocaleQuery(query, options);
99
+ const suffix = query.size === 0 ? "" : `?${query}`;
100
+ const body = await this.#request(`/api/access/globals/${encodeURIComponent(slug)}${suffix}`, {
101
+ method: "POST",
102
+ body: JSON.stringify(options?.data === undefined ? {} : { data: options.data }),
103
+ }, options);
104
+ return accessCapabilitiesFromEnvelope(body);
105
+ }
106
+ async createPreviewToken(collection, id, options) {
107
+ const body = await this.#request(`/api/preview/collections/${encodeURIComponent(collection)}/${encodeDocumentID(id)}/token`, { method: "POST" }, options);
108
+ return previewTokenFromEnvelope(body);
109
+ }
110
+ async revokePreviewToken(token, options) {
111
+ const body = await this.#request("/api/preview/token/revoke", { method: "POST", body: JSON.stringify({ token }) }, options);
112
+ if (!isRecord(body) || body.success !== true) {
113
+ throw invalidSuccessEnvelope("preview token revocation");
114
+ }
115
+ return { success: true };
116
+ }
117
+ async preview(collection, id, token, options) {
118
+ const body = await this.#request(`/api/preview/collections/${encodeURIComponent(collection)}/${encodeDocumentID(id)}`, { method: "GET" }, previewRequestOptions(options, token));
119
+ return documentFromEnvelope(body);
120
+ }
121
+ async createGlobalPreviewToken(slug, options) {
122
+ const body = await this.#request(`/api/preview/globals/${encodeURIComponent(slug)}/token`, { method: "POST" }, options);
123
+ return previewTokenFromEnvelope(body);
124
+ }
125
+ async previewGlobal(slug, token, options) {
126
+ const body = await this.#request(`/api/preview/globals/${encodeURIComponent(slug)}`, { method: "GET" }, previewRequestOptions(options, token));
127
+ return documentFromEnvelope(body);
128
+ }
129
+ async login(collection, credentials, options) {
130
+ const body = await this.#request(`/api/auth/${encodeURIComponent(collection)}/login`, { method: "POST", body: JSON.stringify(credentials) }, options);
131
+ return sessionFromEnvelope(body);
132
+ }
133
+ async session(options) {
134
+ const body = await this.#request("/api/auth/me", { method: "GET" }, options);
135
+ return sessionFromEnvelope(body);
136
+ }
137
+ async refreshSession(options) {
138
+ const body = await this.#request("/api/auth/refresh", { method: "POST" }, options);
139
+ return sessionFromEnvelope(body);
140
+ }
141
+ async logout(options) {
142
+ const body = await this.#request("/api/auth/logout", { method: "POST" }, options);
143
+ if (!isRecord(body) || body.loggedOut !== true) {
144
+ throw invalidSuccessEnvelope("logout");
145
+ }
146
+ return { loggedOut: true };
147
+ }
148
+ async logoutAll(options) {
149
+ const body = await this.#request("/api/auth/logout-all", { method: "POST" }, options);
150
+ if (!isRecord(body) || body.loggedOut !== true) {
151
+ throw invalidSuccessEnvelope("logout-all");
152
+ }
153
+ return { loggedOut: true };
154
+ }
155
+ async sessions(options) {
156
+ const body = await this.#request("/api/auth/sessions", { method: "GET" }, options);
157
+ if (!isRecord(body) ||
158
+ !Array.isArray(body.sessions) ||
159
+ !body.sessions.every(isAuthSessionInfo)) {
160
+ throw invalidSuccessEnvelope("sessions");
161
+ }
162
+ return body.sessions;
163
+ }
164
+ async revokeSession(id, options) {
165
+ const body = await this.#request(`/api/auth/sessions/${encodeURIComponent(id)}`, { method: "DELETE" }, options);
166
+ if (!isRecord(body) || body.id !== id || body.deleted !== true) {
167
+ throw invalidSuccessEnvelope("session revocation");
168
+ }
169
+ return { id, deleted: true };
170
+ }
171
+ async requestPasswordReset(collection, email, options) {
172
+ return this.#authAction(collection, "forgot-password", { email }, options);
173
+ }
174
+ async resetPassword(collection, token, password, options) {
175
+ return this.#authAction(collection, "reset-password", { token, password }, options);
176
+ }
177
+ async authBootstrap(collection, options) {
178
+ const body = await this.#request(`/api/auth/${encodeURIComponent(collection)}/bootstrap`, { method: "GET" }, options);
179
+ if (!isRecord(body) || typeof body.available !== "boolean") {
180
+ throw invalidSuccessEnvelope("auth bootstrap");
181
+ }
182
+ return { available: body.available };
183
+ }
184
+ async createAuthUser(collection, data, password, options) {
185
+ const query = new URLSearchParams();
186
+ appendLocaleQuery(query, options);
187
+ const suffix = query.size === 0 ? "" : `?${query}`;
188
+ const body = await this.#request(`/api/auth/${encodeURIComponent(collection)}/create-user${suffix}`, { method: "POST", body: JSON.stringify({ data, password }) }, options);
189
+ return documentFromEnvelope(body);
190
+ }
191
+ async requestVerification(collection, email, options) {
192
+ return this.#authAction(collection, "request-verification", { email }, options);
193
+ }
194
+ async verifyEmail(collection, token, options) {
195
+ return this.#authAction(collection, "verify", { token }, options);
196
+ }
197
+ async changePassword(currentPassword, password, options) {
198
+ const body = await this.#request("/api/auth/change-password", { method: "POST", body: JSON.stringify({ currentPassword, password }) }, options);
199
+ if (!isRecord(body) || body.success !== true) {
200
+ throw invalidSuccessEnvelope("password change");
201
+ }
202
+ return { success: true };
203
+ }
204
+ async createAPIKey(input, options) {
205
+ const body = await this.#request("/api/auth/api-keys", { method: "POST", body: JSON.stringify(input) }, options);
206
+ if (!isRecord(body) || !isAPIKey(body.apiKey, true)) {
207
+ throw invalidSuccessEnvelope("API key");
208
+ }
209
+ return body.apiKey;
210
+ }
211
+ async apiKeys(options) {
212
+ const body = await this.#request("/api/auth/api-keys", { method: "GET" }, options);
213
+ if (!isRecord(body) ||
214
+ !Array.isArray(body.apiKeys) ||
215
+ !body.apiKeys.every((key) => isAPIKey(key, false))) {
216
+ throw invalidSuccessEnvelope("API keys");
217
+ }
218
+ return body.apiKeys;
219
+ }
220
+ async revokeAPIKey(id, options) {
221
+ const body = await this.#request(`/api/auth/api-keys/${encodeURIComponent(id)}`, { method: "DELETE" }, options);
222
+ if (!isRecord(body) || body.id !== id || body.deleted !== true) {
223
+ throw invalidSuccessEnvelope("API key revocation");
224
+ }
225
+ return { id, deleted: true };
226
+ }
227
+ async forceUnlock(collection, id, options) {
228
+ const body = await this.#request(`/api/auth/${encodeURIComponent(collection)}/${encodeDocumentID(id)}/unlock`, { method: "POST" }, options);
229
+ if (!isRecord(body) || body.success !== true) {
230
+ throw invalidSuccessEnvelope("account unlock");
231
+ }
232
+ return { success: true };
233
+ }
234
+ async documentLock(collection, id, options) {
235
+ const body = await this.#request(`/api/collections/${encodeURIComponent(collection)}/${encodeDocumentID(id)}/lock`, { method: "GET" }, options);
236
+ return documentLockFromEnvelope(body);
237
+ }
238
+ async acquireDocumentLock(collection, id, takeover = false, options) {
239
+ const body = await this.#request(`/api/collections/${encodeURIComponent(collection)}/${encodeDocumentID(id)}/lock`, { method: "POST", body: JSON.stringify({ takeover }) }, options);
240
+ return documentLockFromEnvelope(body);
241
+ }
242
+ async releaseDocumentLock(collection, id, options) {
243
+ const body = await this.#request(`/api/collections/${encodeURIComponent(collection)}/${encodeDocumentID(id)}/lock`, { method: "DELETE" }, options);
244
+ if (!isRecord(body) || body.id !== id || body.deleted !== true) {
245
+ throw invalidSuccessEnvelope("document lock release");
246
+ }
247
+ return { id, deleted: true };
248
+ }
249
+ async #authAction(collection, action, input, options) {
250
+ const body = await this.#request(`/api/auth/${encodeURIComponent(collection)}/${action}`, { method: "POST", body: JSON.stringify(input) }, options);
251
+ if (!isRecord(body) || body.success !== true) {
252
+ throw invalidSuccessEnvelope("auth action");
253
+ }
254
+ return { success: true };
255
+ }
256
+ async list(collection, options) {
257
+ const query = new URLSearchParams();
258
+ if (options?.page !== undefined)
259
+ query.set("page", String(options.page));
260
+ if (options?.limit !== undefined)
261
+ query.set("limit", String(options.limit));
262
+ if (options?.depth !== undefined)
263
+ query.set("depth", String(options.depth));
264
+ if (options?.where !== undefined)
265
+ query.set("where", JSON.stringify(options.where));
266
+ if (options?.select !== undefined)
267
+ query.set("select", JSON.stringify(options.select));
268
+ if (options?.populate !== undefined)
269
+ query.set("populate", JSON.stringify(options.populate));
270
+ if (options?.trash === true)
271
+ query.set("trash", "true");
272
+ appendLocaleQuery(query, options);
273
+ for (const sort of options?.sort ?? [])
274
+ query.append("sort", sort);
275
+ const suffix = query.size === 0 ? "" : `?${query}`;
276
+ const body = await this.#request(`/api/collections/${encodeURIComponent(collection)}${suffix}`, { method: "GET" }, options);
277
+ if (!isPageEnvelope(body)) {
278
+ throw invalidSuccessEnvelope("page");
279
+ }
280
+ return body;
281
+ }
282
+ async count(collection, options) {
283
+ const query = new URLSearchParams();
284
+ if (options?.where !== undefined)
285
+ query.set("where", JSON.stringify(options.where));
286
+ if (options?.trash === true)
287
+ query.set("trash", "true");
288
+ appendLocaleQuery(query, options);
289
+ const suffix = query.size === 0 ? "" : `?${query}`;
290
+ const body = await this.#request(`/api/collections/${encodeURIComponent(collection)}/count${suffix}`, { method: "GET" }, options);
291
+ if (!isRecord(body) || typeof body.totalDocs !== "number") {
292
+ throw invalidSuccessEnvelope("count");
293
+ }
294
+ return { totalDocs: body.totalDocs };
295
+ }
296
+ async find(collection, id, options) {
297
+ const query = new URLSearchParams();
298
+ if (options?.depth !== undefined)
299
+ query.set("depth", String(options.depth));
300
+ if (options?.select !== undefined)
301
+ query.set("select", JSON.stringify(options.select));
302
+ if (options?.populate !== undefined)
303
+ query.set("populate", JSON.stringify(options.populate));
304
+ appendLocaleQuery(query, options);
305
+ const suffix = query.size === 0 ? "" : `?${query}`;
306
+ const body = await this.#request(`/api/collections/${encodeURIComponent(collection)}/${encodeDocumentID(id)}${suffix}`, { method: "GET" }, options);
307
+ return documentFromEnvelope(body);
308
+ }
309
+ async create(collection, data, options) {
310
+ const query = new URLSearchParams();
311
+ appendLocaleQuery(query, options);
312
+ appendDraftQuery(query, options);
313
+ const suffix = query.size === 0 ? "" : `?${query}`;
314
+ const body = await this.#request(`/api/collections/${encodeURIComponent(collection)}${suffix}`, { method: "POST", body: JSON.stringify(data) }, options);
315
+ return documentFromEnvelope(body);
316
+ }
317
+ async duplicate(collection, id, overrides = {}, options) {
318
+ const query = new URLSearchParams();
319
+ appendLocaleQuery(query, options);
320
+ const suffix = query.size === 0 ? "" : `?${query}`;
321
+ const body = await this.#request(`/api/collections/${encodeURIComponent(collection)}/${encodeDocumentID(id)}/duplicate${suffix}`, { method: "POST", body: JSON.stringify(overrides) }, options);
322
+ return documentFromEnvelope(body);
323
+ }
324
+ async copyLocale(collection, id, input, options) {
325
+ const revision = revisionHeaders(options);
326
+ const body = await this.#request(`/api/collections/${encodeURIComponent(collection)}/${encodeDocumentID(id)}/copy-locale`, {
327
+ method: "POST",
328
+ body: JSON.stringify(input),
329
+ ...(revision === undefined ? {} : { headers: revision }),
330
+ }, options);
331
+ return documentFromEnvelope(body);
332
+ }
333
+ async update(collection, id, data, options) {
334
+ const revision = revisionHeaders(options);
335
+ const query = new URLSearchParams();
336
+ appendLocaleQuery(query, options);
337
+ const suffix = query.size === 0 ? "" : `?${query}`;
338
+ const body = await this.#request(`/api/collections/${encodeURIComponent(collection)}/${encodeDocumentID(id)}${suffix}`, {
339
+ method: "PATCH",
340
+ body: JSON.stringify(data),
341
+ ...(revision === undefined ? {} : { headers: revision }),
342
+ }, options);
343
+ return documentFromEnvelope(body);
344
+ }
345
+ async upload(collection, file, options) {
346
+ const form = new FormData();
347
+ form.set("file", file);
348
+ if (options?.data !== undefined)
349
+ form.set("data", JSON.stringify(options.data));
350
+ const query = new URLSearchParams();
351
+ appendLocaleQuery(query, options);
352
+ const suffix = query.size === 0 ? "" : `?${query}`;
353
+ const body = await this.#request("/api/collections/" + encodeURIComponent(collection) + suffix, { method: "POST", body: form }, options);
354
+ return documentFromEnvelope(body);
355
+ }
356
+ async uploadFromURL(collection, url, options) {
357
+ const query = new URLSearchParams();
358
+ appendLocaleQuery(query, options);
359
+ const suffix = query.size === 0 ? "" : `?${query}`;
360
+ const body = await this.#request(`/api/collections/${encodeURIComponent(collection)}/remote-upload${suffix}`, { method: "POST", body: JSON.stringify({ url, data: options?.data ?? {} }) }, options);
361
+ return documentFromEnvelope(body);
362
+ }
363
+ async updateUploadImage(collection, id, input, options) {
364
+ const revision = revisionHeaders(options);
365
+ const body = await this.#request(`/api/collections/${encodeURIComponent(collection)}/${encodeDocumentID(id)}/image`, {
366
+ method: "PATCH",
367
+ body: JSON.stringify(input),
368
+ ...(revision === undefined ? {} : { headers: revision }),
369
+ }, options);
370
+ return documentFromEnvelope(body);
371
+ }
372
+ async versions(collection, id, options) {
373
+ const query = new URLSearchParams();
374
+ appendLocaleQuery(query, options);
375
+ const suffix = query.size === 0 ? "" : `?${query}`;
376
+ const body = await this.#request("/api/collections/" +
377
+ encodeURIComponent(collection) +
378
+ "/" +
379
+ encodeDocumentID(id) +
380
+ `/versions${suffix}`, { method: "GET" }, options);
381
+ if (!isRecord(body) || !Array.isArray(body.versions)) {
382
+ throw invalidSuccessEnvelope("versions");
383
+ }
384
+ return body.versions;
385
+ }
386
+ async version(collection, id, revision, options) {
387
+ const query = new URLSearchParams();
388
+ appendLocaleQuery(query, options);
389
+ const suffix = query.size === 0 ? "" : `?${query}`;
390
+ const body = await this.#request(`/api/collections/${encodeURIComponent(collection)}/${encodeDocumentID(id)}/versions/${revision}${suffix}`, { method: "GET" }, options);
391
+ if (!isRecord(body) || !isRecord(body.version)) {
392
+ throw invalidSuccessEnvelope("version");
393
+ }
394
+ return body.version;
395
+ }
396
+ async schedulePublish(collection, id, runAt, options) {
397
+ const headers = revisionHeaders(options);
398
+ const body = await this.#request(`/api/collections/${encodeURIComponent(collection)}/${encodeDocumentID(id)}/schedule`, {
399
+ method: "POST",
400
+ body: JSON.stringify({ runAt: runAt instanceof Date ? runAt.toISOString() : runAt }),
401
+ ...(headers === undefined ? {} : { headers }),
402
+ }, options);
403
+ if (!isRecord(body) || !isScheduledPublish(body.scheduledPublish)) {
404
+ throw invalidSuccessEnvelope("scheduled publish");
405
+ }
406
+ return body.scheduledPublish;
407
+ }
408
+ async scheduledPublishes(collection, id, options) {
409
+ const body = await this.#request(`/api/collections/${encodeURIComponent(collection)}/${encodeDocumentID(id)}/schedule`, { method: "GET" }, options);
410
+ if (!isRecord(body) ||
411
+ !Array.isArray(body.scheduledPublishes) ||
412
+ !body.scheduledPublishes.every(isScheduledPublish)) {
413
+ throw invalidSuccessEnvelope("scheduled publishes");
414
+ }
415
+ return body.scheduledPublishes;
416
+ }
417
+ async cancelScheduledPublish(collection, id, jobID, options) {
418
+ const body = await this.#request(`/api/collections/${encodeURIComponent(collection)}/${encodeDocumentID(id)}/schedule/${encodeURIComponent(jobID)}`, { method: "DELETE" }, options);
419
+ if (!isRecord(body) || body.id !== jobID || body.deleted !== true) {
420
+ throw invalidSuccessEnvelope("scheduled publish cancellation");
421
+ }
422
+ return { id: jobID, deleted: true };
423
+ }
424
+ async publish(collection, id, options) {
425
+ return this.#versionMutation(collection, id, "publish", options);
426
+ }
427
+ async publishChanges(collection, id, data, options) {
428
+ return this.#versionMutation(collection, id, "publish", options, data);
429
+ }
430
+ async unpublish(collection, id, options) {
431
+ return this.#versionMutation(collection, id, "unpublish", options);
432
+ }
433
+ async restore(collection, id, revision, options) {
434
+ const query = options?.draft === true ? "?draft=true" : "";
435
+ return this.#versionMutation(collection, id, "restore/" + revision + query, options);
436
+ }
437
+ async #versionMutation(collection, id, action, options, data) {
438
+ const revision = revisionHeaders(options);
439
+ const [actionPath, encodedQuery = ""] = action.split("?", 2);
440
+ const query = new URLSearchParams(encodedQuery);
441
+ appendLocaleQuery(query, options);
442
+ const suffix = query.size === 0 ? "" : `?${query}`;
443
+ const body = await this.#request("/api/collections/" +
444
+ encodeURIComponent(collection) +
445
+ "/" +
446
+ encodeDocumentID(id) +
447
+ "/" +
448
+ actionPath +
449
+ suffix, {
450
+ method: "POST",
451
+ ...(data === undefined ? {} : { body: JSON.stringify(data) }),
452
+ ...(revision === undefined ? {} : { headers: revision }),
453
+ }, options);
454
+ return documentFromEnvelope(body);
455
+ }
456
+ async delete(collection, id, options) {
457
+ const query = new URLSearchParams();
458
+ appendLocaleQuery(query, options);
459
+ const suffix = query.size === 0 ? "" : `?${query}`;
460
+ const body = await this.#request(`/api/collections/${encodeURIComponent(collection)}/${encodeDocumentID(id)}${suffix}`, { method: "DELETE" }, options);
461
+ if (!isRecord(body) || typeof body.id !== "string" || body.deleted !== true) {
462
+ throw invalidSuccessEnvelope("delete");
463
+ }
464
+ return { id: body.id, deleted: true };
465
+ }
466
+ async mutateJoin(collection, id, field, input, options) {
467
+ const query = new URLSearchParams();
468
+ appendLocaleQuery(query, options);
469
+ const suffix = query.size === 0 ? "" : `?${query}`;
470
+ const body = await this.#request(`/api/collections/${encodeURIComponent(collection)}/${encodeDocumentID(id)}/joins/${encodeURIComponent(field)}${suffix}`, { method: "PATCH", body: JSON.stringify(input) }, options);
471
+ if (!isRecord(body) ||
472
+ !isRecord(body.doc) ||
473
+ typeof body.added !== "number" ||
474
+ !Number.isInteger(body.added) ||
475
+ typeof body.removed !== "number" ||
476
+ !Number.isInteger(body.removed)) {
477
+ throw invalidSuccessEnvelope("join mutation");
478
+ }
479
+ return {
480
+ doc: body.doc,
481
+ added: body.added,
482
+ removed: body.removed,
483
+ };
484
+ }
485
+ async bulkUpdate(collection, ids, data, options) {
486
+ return this.#bulk(collection, "update", ids, data, options);
487
+ }
488
+ async bulkPublish(collection, ids, options) {
489
+ return this.#bulk(collection, "publish", ids, undefined, options);
490
+ }
491
+ async bulkUnpublish(collection, ids, options) {
492
+ return this.#bulk(collection, "unpublish", ids, undefined, options);
493
+ }
494
+ async bulkDelete(collection, ids, options) {
495
+ return this.#bulk(collection, "delete", ids, undefined, options);
496
+ }
497
+ async bulkRestoreDeleted(collection, ids, options) {
498
+ return this.#bulk(collection, "restoreDeleted", ids, undefined, options);
499
+ }
500
+ async bulkDeletePermanent(collection, ids, options) {
501
+ return this.#bulk(collection, "deletePermanent", ids, undefined, options);
502
+ }
503
+ async emptyTrash(collection, options) {
504
+ const query = new URLSearchParams();
505
+ query.set("trash", "true");
506
+ appendLocaleQuery(query, options);
507
+ const suffix = `?${query}`;
508
+ const body = await this.#request(`/api/collections/${encodeURIComponent(collection)}${suffix}`, { method: "DELETE" }, options);
509
+ if (!isRecord(body) || !Array.isArray(body.docs) || !body.docs.every(isRecord)) {
510
+ throw invalidSuccessEnvelope("empty trash");
511
+ }
512
+ return body.docs;
513
+ }
514
+ async #bulk(collection, action, ids, data, options) {
515
+ const query = new URLSearchParams();
516
+ appendLocaleQuery(query, options);
517
+ const suffix = query.size === 0 ? "" : `?${query}`;
518
+ const body = await this.#request(`/api/collections/${encodeURIComponent(collection)}/bulk${suffix}`, {
519
+ method: "POST",
520
+ body: JSON.stringify({ action, ids, ...(data === undefined ? {} : { data }) }),
521
+ }, options);
522
+ if (!isRecord(body) || !Array.isArray(body.docs) || !body.docs.every(isRecord)) {
523
+ throw invalidSuccessEnvelope("bulk operation");
524
+ }
525
+ return body.docs;
526
+ }
527
+ async restoreDeleted(collection, id, options) {
528
+ const query = new URLSearchParams();
529
+ appendLocaleQuery(query, options);
530
+ const suffix = query.size === 0 ? "" : `?${query}`;
531
+ const body = await this.#request(`/api/collections/${encodeURIComponent(collection)}/${encodeDocumentID(id)}/restore-deleted${suffix}`, { method: "POST" }, options);
532
+ return documentFromEnvelope(body);
533
+ }
534
+ async deletePermanent(collection, id, options) {
535
+ const query = new URLSearchParams();
536
+ appendLocaleQuery(query, options);
537
+ const suffix = query.size === 0 ? "" : `?${query}`;
538
+ const body = await this.#request(`/api/collections/${encodeURIComponent(collection)}/${encodeDocumentID(id)}/permanent${suffix}`, { method: "DELETE" }, options);
539
+ if (!isRecord(body) || typeof body.id !== "string" || body.deleted !== true) {
540
+ throw invalidSuccessEnvelope("permanent delete");
541
+ }
542
+ return { id: body.id, deleted: true };
543
+ }
544
+ async global(slug, options) {
545
+ const query = new URLSearchParams();
546
+ if (options?.depth !== undefined)
547
+ query.set("depth", String(options.depth));
548
+ if (options?.select !== undefined)
549
+ query.set("select", JSON.stringify(options.select));
550
+ if (options?.populate !== undefined)
551
+ query.set("populate", JSON.stringify(options.populate));
552
+ appendLocaleQuery(query, options);
553
+ const suffix = query.size === 0 ? "" : `?${query}`;
554
+ const body = await this.#request(`/api/globals/${encodeURIComponent(slug)}${suffix}`, { method: "GET" }, options);
555
+ return documentFromEnvelope(body);
556
+ }
557
+ async updateGlobal(slug, data, options) {
558
+ const revision = revisionHeaders(options);
559
+ const query = new URLSearchParams();
560
+ appendLocaleQuery(query, options);
561
+ const suffix = query.size === 0 ? "" : `?${query}`;
562
+ const body = await this.#request(`/api/globals/${encodeURIComponent(slug)}${suffix}`, {
563
+ method: "PATCH",
564
+ body: JSON.stringify(data),
565
+ ...(revision === undefined ? {} : { headers: revision }),
566
+ }, options);
567
+ return documentFromEnvelope(body);
568
+ }
569
+ async copyGlobalLocale(slug, input, options) {
570
+ const revision = revisionHeaders(options);
571
+ const body = await this.#request(`/api/globals/${encodeURIComponent(slug)}/copy-locale`, {
572
+ method: "POST",
573
+ body: JSON.stringify(input),
574
+ ...(revision === undefined ? {} : { headers: revision }),
575
+ }, options);
576
+ return documentFromEnvelope(body);
577
+ }
578
+ async globalVersions(slug, options) {
579
+ const query = new URLSearchParams();
580
+ appendLocaleQuery(query, options);
581
+ const suffix = query.size === 0 ? "" : `?${query}`;
582
+ const body = await this.#request(`/api/globals/${encodeURIComponent(slug)}/versions${suffix}`, { method: "GET" }, options);
583
+ if (!isRecord(body) || !Array.isArray(body.versions)) {
584
+ throw invalidSuccessEnvelope("global versions");
585
+ }
586
+ return body.versions;
587
+ }
588
+ async globalVersion(slug, revision, options) {
589
+ const query = new URLSearchParams();
590
+ appendLocaleQuery(query, options);
591
+ const suffix = query.size === 0 ? "" : `?${query}`;
592
+ const body = await this.#request(`/api/globals/${encodeURIComponent(slug)}/versions/${revision}${suffix}`, { method: "GET" }, options);
593
+ if (!isRecord(body) || !isRecord(body.version)) {
594
+ throw invalidSuccessEnvelope("global version");
595
+ }
596
+ return body.version;
597
+ }
598
+ async publishGlobal(slug, options) {
599
+ return this.#globalVersionMutation(slug, "publish", options);
600
+ }
601
+ async publishGlobalChanges(slug, data, options) {
602
+ return this.#globalVersionMutation(slug, "publish", options, data);
603
+ }
604
+ async unpublishGlobal(slug, options) {
605
+ return this.#globalVersionMutation(slug, "unpublish", options);
606
+ }
607
+ async restoreGlobal(slug, revision, options) {
608
+ return this.#globalVersionMutation(slug, `restore/${revision}${options?.draft === true ? "?draft=true" : ""}`, options);
609
+ }
610
+ async #globalVersionMutation(slug, action, options, data) {
611
+ const revision = revisionHeaders(options);
612
+ const [actionPath, encodedQuery = ""] = action.split("?", 2);
613
+ const query = new URLSearchParams(encodedQuery);
614
+ appendLocaleQuery(query, options);
615
+ const suffix = query.size === 0 ? "" : `?${query}`;
616
+ const body = await this.#request(`/api/globals/${encodeURIComponent(slug)}/${actionPath}${suffix}`, {
617
+ method: "POST",
618
+ ...(data === undefined ? {} : { body: JSON.stringify(data) }),
619
+ ...(revision === undefined ? {} : { headers: revision }),
620
+ }, options);
621
+ return documentFromEnvelope(body);
622
+ }
623
+ async #request(path, init, options) {
624
+ const response = await this.#response(path, init, options, true);
625
+ if (!response.ok) {
626
+ throw await RiduError.fromResponse(response);
627
+ }
628
+ try {
629
+ return await response.json();
630
+ }
631
+ catch {
632
+ throw invalidSuccessEnvelope("JSON");
633
+ }
634
+ }
635
+ async #response(path, init, options, defaultJSONContentType) {
636
+ if (!path.startsWith("/") || path.startsWith("//") || path.includes("#")) {
637
+ throw new TypeError("request path must be an absolute-path reference");
638
+ }
639
+ const target = new URL(path, this.#baseURL);
640
+ if (target.origin !== new URL(this.#baseURL).origin || target.hash !== "") {
641
+ throw new TypeError("request path must stay on the configured origin and omit fragments");
642
+ }
643
+ const headers = new Headers(await resolveHeaders(this.#headers));
644
+ for (const [name, value] of new Headers(init.headers))
645
+ headers.set(name, value);
646
+ for (const [name, value] of new Headers(options?.headers))
647
+ headers.set(name, value);
648
+ if (defaultJSONContentType &&
649
+ init.body !== undefined &&
650
+ !(init.body instanceof FormData) &&
651
+ !headers.has("content-type")) {
652
+ headers.set("content-type", "application/json");
653
+ }
654
+ const request = new Request(target, {
655
+ ...init,
656
+ headers,
657
+ credentials: this.#credentials,
658
+ ...(options?.signal === undefined ? {} : { signal: options.signal }),
659
+ ...(options?.keepalive === undefined ? {} : { keepalive: options.keepalive }),
660
+ });
661
+ return this.#dispatch(request);
662
+ }
663
+ }
664
+ function revisionHeaders(options) {
665
+ return options?.revision === undefined ? undefined : { "If-Match": '"' + options.revision + '"' };
666
+ }
667
+ function encodeDocumentID(id) {
668
+ return encodeURIComponent(id);
669
+ }
670
+ function previewRequestOptions(options, token) {
671
+ const headers = new Headers(options?.headers);
672
+ headers.set("Authorization", `Bearer ${token}`);
673
+ return { ...options, headers };
674
+ }
675
+ function composeMiddleware(middleware, terminal) {
676
+ return middleware.reduceRight((next, current) => (request) => current(request, next), terminal);
677
+ }
678
+ async function resolveHeaders(headers) {
679
+ return typeof headers === "function" ? await headers() : headers;
680
+ }
681
+ function documentFromEnvelope(value) {
682
+ if (!isRecord(value) || !("doc" in value)) {
683
+ throw invalidSuccessEnvelope("document");
684
+ }
685
+ return value.doc;
686
+ }
687
+ function previewTokenFromEnvelope(value) {
688
+ if (!isRecord(value) ||
689
+ !isRecord(value.previewToken) ||
690
+ typeof value.previewToken.token !== "string" ||
691
+ (value.previewToken.resource !== "collection" && value.previewToken.resource !== "global") ||
692
+ typeof value.previewToken.slug !== "string" ||
693
+ typeof value.previewToken.documentId !== "string" ||
694
+ typeof value.previewToken.expiresAt !== "string") {
695
+ throw invalidSuccessEnvelope("preview token");
696
+ }
697
+ return value.previewToken;
698
+ }
699
+ function accessCapabilitiesFromEnvelope(value) {
700
+ if (!isRecord(value) || !isOperationCapabilities(value.operations) || !isRecord(value.fields)) {
701
+ throw invalidSuccessEnvelope("access capabilities");
702
+ }
703
+ const fields = {};
704
+ for (const [path, capability] of Object.entries(value.fields)) {
705
+ if (!isFieldCapabilities(capability))
706
+ throw invalidSuccessEnvelope("field capabilities");
707
+ fields[path] = capability;
708
+ }
709
+ return { operations: value.operations, fields };
710
+ }
711
+ function collectionSelectionFromEnvelope(value) {
712
+ if (!isRecord(value) ||
713
+ !Array.isArray(value.items) ||
714
+ value.items.length > 100 ||
715
+ typeof value.totalDocs !== "number" ||
716
+ !Number.isInteger(value.totalDocs) ||
717
+ value.totalDocs !== value.items.length) {
718
+ throw invalidSuccessEnvelope("filtered collection selection");
719
+ }
720
+ const seen = new Set();
721
+ let previousID;
722
+ const items = value.items.map((item) => {
723
+ if (!isRecord(item) ||
724
+ typeof item.id !== "string" ||
725
+ item.id.length === 0 ||
726
+ !isRecord(item.access)) {
727
+ throw invalidSuccessEnvelope("filtered collection selection item");
728
+ }
729
+ if (seen.has(item.id) || (previousID !== undefined && compareUTF8(previousID, item.id) >= 0)) {
730
+ throw invalidSuccessEnvelope("filtered collection selection IDs");
731
+ }
732
+ seen.add(item.id);
733
+ previousID = item.id;
734
+ return { id: item.id, access: accessCapabilitiesFromEnvelope(item.access) };
735
+ });
736
+ return { items, totalDocs: value.totalDocs };
737
+ }
738
+ function compareUTF8(left, right) {
739
+ const encoder = new TextEncoder();
740
+ const leftBytes = encoder.encode(left);
741
+ const rightBytes = encoder.encode(right);
742
+ const length = Math.min(leftBytes.length, rightBytes.length);
743
+ for (let index = 0; index < length; index += 1) {
744
+ const difference = (leftBytes[index] ?? 0) - (rightBytes[index] ?? 0);
745
+ if (difference !== 0)
746
+ return difference;
747
+ }
748
+ return leftBytes.length - rightBytes.length;
749
+ }
750
+ function documentLockFromEnvelope(value) {
751
+ if (!isRecord(value) ||
752
+ typeof value.owned !== "boolean" ||
753
+ typeof value.acquired !== "boolean" ||
754
+ typeof value.canTakeOver !== "boolean" ||
755
+ (value.lock !== null &&
756
+ (!isRecord(value.lock) ||
757
+ typeof value.lock.documentId !== "string" ||
758
+ typeof value.lock.ownerId !== "string" ||
759
+ typeof value.lock.ownerLabel !== "string" ||
760
+ typeof value.lock.createdAt !== "string" ||
761
+ typeof value.lock.updatedAt !== "string" ||
762
+ typeof value.lock.expiresAt !== "string"))) {
763
+ throw invalidSuccessEnvelope("document lock");
764
+ }
765
+ return value;
766
+ }
767
+ function isOperationCapabilities(value) {
768
+ return (isRecord(value) &&
769
+ [
770
+ "admin",
771
+ "create",
772
+ "read",
773
+ "readVersions",
774
+ "update",
775
+ "delete",
776
+ "duplicate",
777
+ "publish",
778
+ "unpublish",
779
+ "restoreDeleted",
780
+ "deletePermanent",
781
+ "selectAll",
782
+ ].every((key) => typeof value[key] === "boolean"));
783
+ }
784
+ function isFieldCapabilities(value) {
785
+ return (isRecord(value) &&
786
+ typeof value.read === "boolean" &&
787
+ typeof value.create === "boolean" &&
788
+ typeof value.update === "boolean");
789
+ }
790
+ function sessionFromEnvelope(value) {
791
+ if (!isRecord(value) ||
792
+ !isRecord(value.session) ||
793
+ !("user" in value.session) ||
794
+ typeof value.session.id !== "string" ||
795
+ typeof value.session.collection !== "string" ||
796
+ typeof value.session.expiresAt !== "string") {
797
+ throw invalidSuccessEnvelope("session");
798
+ }
799
+ return {
800
+ id: value.session.id,
801
+ collection: value.session.collection,
802
+ user: value.session.user,
803
+ expiresAt: value.session.expiresAt,
804
+ };
805
+ }
806
+ function isAuthSessionInfo(value) {
807
+ return (isRecord(value) &&
808
+ typeof value.id === "string" &&
809
+ typeof value.createdAt === "string" &&
810
+ typeof value.lastSeenAt === "string" &&
811
+ typeof value.expiresAt === "string" &&
812
+ (value.ipAddress === undefined || typeof value.ipAddress === "string") &&
813
+ (value.userAgent === undefined || typeof value.userAgent === "string") &&
814
+ typeof value.current === "boolean");
815
+ }
816
+ function isAPIKey(value, includeSecret) {
817
+ return (isRecord(value) &&
818
+ typeof value.id === "string" &&
819
+ typeof value.name === "string" &&
820
+ typeof value.createdAt === "string" &&
821
+ (!includeSecret || typeof value.key === "string") &&
822
+ (value.lastUsedAt === undefined || typeof value.lastUsedAt === "string") &&
823
+ (value.expiresAt === undefined || typeof value.expiresAt === "string"));
824
+ }
825
+ function isScheduledPublish(value) {
826
+ return (isRecord(value) &&
827
+ typeof value.id === "string" &&
828
+ typeof value.documentId === "string" &&
829
+ typeof value.expectedRevision === "number" &&
830
+ typeof value.runAt === "string" &&
831
+ typeof value.attempts === "number" &&
832
+ (value.lastError === undefined || typeof value.lastError === "string") &&
833
+ typeof value.createdAt === "string");
834
+ }
835
+ function appendLocaleQuery(query, options) {
836
+ if (options?.locale !== undefined)
837
+ query.set("locale", options.locale);
838
+ if (options?.fallbackLocale === false) {
839
+ query.set("fallback-locale", "false");
840
+ }
841
+ else if (typeof options?.fallbackLocale === "string") {
842
+ query.set("fallback-locale", options.fallbackLocale);
843
+ }
844
+ else if (options?.fallbackLocale !== undefined) {
845
+ query.set("fallback-locale", options.fallbackLocale.join(","));
846
+ }
847
+ }
848
+ function appendDraftQuery(query, options) {
849
+ if (options?.draft !== undefined)
850
+ query.set("draft", String(options.draft));
851
+ }
852
+ function invalidSuccessEnvelope(kind) {
853
+ return new RiduError({
854
+ code: "internal",
855
+ status: 500,
856
+ message: `Server returned an invalid ${kind} response envelope`,
857
+ issues: [],
858
+ });
859
+ }
860
+ function isRecord(value) {
861
+ return typeof value === "object" && value !== null && !Array.isArray(value);
862
+ }