@sanity/client 0.0.0-dev.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.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1225 -0
  3. package/dist/index.browser.cjs +1760 -0
  4. package/dist/index.browser.cjs.map +1 -0
  5. package/dist/index.browser.js +1737 -0
  6. package/dist/index.browser.js.map +1 -0
  7. package/dist/index.cjs +1769 -0
  8. package/dist/index.cjs.js +16 -0
  9. package/dist/index.cjs.map +1 -0
  10. package/dist/index.d.ts +2234 -0
  11. package/dist/index.js +1746 -0
  12. package/dist/index.js.map +1 -0
  13. package/package.json +127 -0
  14. package/src/SanityClient.ts +1253 -0
  15. package/src/assets/AssetsClient.ts +191 -0
  16. package/src/config.ts +96 -0
  17. package/src/data/dataMethods.ts +416 -0
  18. package/src/data/encodeQueryString.ts +29 -0
  19. package/src/data/listen.ts +196 -0
  20. package/src/data/patch.ts +345 -0
  21. package/src/data/transaction.ts +358 -0
  22. package/src/datasets/DatasetsClient.ts +115 -0
  23. package/src/generateHelpUrl.ts +5 -0
  24. package/src/http/browserMiddleware.ts +1 -0
  25. package/src/http/errors.ts +68 -0
  26. package/src/http/nodeMiddleware.ts +11 -0
  27. package/src/http/request.ts +48 -0
  28. package/src/http/requestOptions.ts +33 -0
  29. package/src/index.browser.ts +28 -0
  30. package/src/index.ts +28 -0
  31. package/src/projects/ProjectsClient.ts +61 -0
  32. package/src/types.ts +575 -0
  33. package/src/users/UsersClient.ts +55 -0
  34. package/src/util/defaults.ts +10 -0
  35. package/src/util/getSelection.ts +21 -0
  36. package/src/util/once.ts +14 -0
  37. package/src/util/pick.ts +11 -0
  38. package/src/validators.ts +78 -0
  39. package/src/warnings.ts +30 -0
  40. package/umd/.gitkeep +1 -0
  41. package/umd/sanityClient.js +4840 -0
  42. package/umd/sanityClient.min.js +14 -0
package/README.md ADDED
@@ -0,0 +1,1225 @@
1
+ # @sanity/client
2
+
3
+ [![npm stat](https://img.shields.io/npm/dm/@sanity/client.svg?style=flat-square)](https://npm-stat.com/charts.html?package=@sanity/client)
4
+ [![npm version](https://img.shields.io/npm/v/@sanity/client.svg?style=flat-square)](https://www.npmjs.com/package/@sanity/client)
5
+ [![gzip size][gzip-badge]][bundlephobia]
6
+ [![size][size-badge]][bundlephobia]
7
+
8
+ JavaScript client for Sanity. Works in modern browsers, as well as runtimes like [Node.js], [Bun], [Deno], and [Edge Runtime]
9
+
10
+ ## QuickStart
11
+
12
+ Install the client with a package manager:
13
+
14
+ ```sh
15
+ npm install @sanity/client
16
+ ```
17
+
18
+ Import and create a new client instance, and use its methods to interact with your project's [Content Lake]. Below are some simple examples in plain JavaScript. Read further for more comprehensive documentation.
19
+
20
+ ```js
21
+ // sanity.js
22
+ import {createClient} from '@sanity/client'
23
+ // Import using ESM URL imports in environments that supports it:
24
+ // import {createClient} from 'https://esm.sh/@sanity/client'
25
+
26
+ export const client = createClient({
27
+ projectId: 'your-project-id',
28
+ dataset: 'your-dataset-name',
29
+ useCdn: false, // set to `true` to fetch from edge cache
30
+ apiVersion: '2022-01-12', // use current date (YYYY-MM-DD) to target the latest API version
31
+ // token: process.env.SANITY_SECRET_TOKEN // Only if you want to update content with the client
32
+ })
33
+
34
+ // uses GROQ to query content: https://www.sanity.io/docs/groq
35
+ export async function getPosts() {
36
+ const posts = await client.fetch('*[_type == "post"]')
37
+ return posts
38
+ }
39
+
40
+ export async function createPost(post: Post) {
41
+ const result = client.create(post)
42
+ return result
43
+ }
44
+
45
+ export async function updateDocumentTitle(_id, title) {
46
+ const result = client.patch(_id).set({title})
47
+ return result
48
+ }
49
+ ```
50
+
51
+ # Table of contents
52
+
53
+ - [QuickStart](#quickstart)
54
+ - [Requirements](#requirements)
55
+ - [Installation](#installation)
56
+ - [API](#api)
57
+ - [Creating a client instance](#creating-a-client-instance)
58
+ - [ESM](#esm)
59
+ - [CommonJS](#commonjs)
60
+ - [TypeScript](#typescript)
61
+ - [Bun](#bun)
62
+ - [Deno](#deno)
63
+ - [Edge Runtime](#edge-runtime)
64
+ - [Browser ESM CDN](#browser-esm-cdn)
65
+ - [UMD](#umd)
66
+ - [Specifying API version](#specifying-api-version)
67
+ - [Performing queries](#performing-queries)
68
+ - [Listening to queries](#listening-to-queries)
69
+ - [Fetch a single document](#fetch-a-single-document)
70
+ - [Fetch multiple documents in one go](#fetch-multiple-documents-in-one-go)
71
+ - [Creating documents](#creating-documents)
72
+ - [Creating/replacing documents](#creatingreplacing-documents)
73
+ - [Creating if not already present](#creating-if-not-already-present)
74
+ - [Patch/update a document](#patchupdate-a-document)
75
+ - [Setting a field only if not already present](#setting-a-field-only-if-not-already-present)
76
+ - [Removing/unsetting fields](#removingunsetting-fields)
77
+ - [Incrementing/decrementing numbers](#incrementingdecrementing-numbers)
78
+ - [Patch a document only if revision matches](#patch-a-document-only-if-revision-matches)
79
+ - [Adding elements to an array](#adding-elements-to-an-array)
80
+ - [Appending/prepending elements to an array](#appendingprepending-elements-to-an-array)
81
+ - [Deleting an element from an array](#deleting-an-element-from-an-array)
82
+ - [Delete documents](#delete-documents)
83
+ - [Multiple mutations in a transaction](#multiple-mutations-in-a-transaction)
84
+ - [Clientless patches \& transactions](#clientless-patches--transactions)
85
+ - [Uploading assets](#uploading-assets)
86
+ - [Examples: Uploading assets from Node.js](#examples-uploading-assets-from-nodejs)
87
+ - [Examples: Uploading assets from the Browser](#examples-uploading-assets-from-the-browser)
88
+ - [Examples: Specify image metadata to extract](#examples-specify-image-metadata-to-extract)
89
+ - [Deleting an asset](#deleting-an-asset)
90
+ - [Mutation options](#mutation-options)
91
+ - [Aborting a request](#aborting-a-request)
92
+ - [Get client configuration](#get-client-configuration)
93
+ - [Set client configuration](#set-client-configuration)
94
+ - [Release new version](#release-new-version)
95
+ - [License](#license)
96
+ - [From `v4`](#from-v4)
97
+
98
+ ## Requirements
99
+
100
+ Sanity Client transpiles syntax for [modern browsers]. The JavaScript runtime must support ES6 features such as [class](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes), [rest parameters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/rest_parameters), [spread syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax) and more. Most modern web frameworks, [browsers][modern browsers], and developer tooling supports ES6 today.
101
+
102
+ [For legacy ES5 environments we recommend v4.](https://github.com/sanity-io/client/tree/v4.0.0#sanityclient)
103
+
104
+ ## Installation
105
+
106
+ The client can be installed from [npm]:
107
+
108
+ ```sh
109
+ npm install @sanity/client
110
+
111
+ # Alternative package managers
112
+ yarn add @sanity/client
113
+ pnpm install @sanity/client
114
+ ```
115
+
116
+ ## API
117
+
118
+ ### Creating a client instance
119
+
120
+ `const client = createClient(options)`
121
+
122
+ Initializes a new Sanity Client. Required options are `projectId`, `dataset`, and `apiVersion`. Setting a value for `useCdn` is encouraged. Typically you want to have it as `false` in development to always fetch the freshest content and `true` in production environments so that content is fetched from the distributed cache. [You can learn more about the API CDN here][api-cdn].
123
+
124
+ #### [ESM](https://hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-dive/)
125
+
126
+ ```js
127
+ import {createClient} from '@sanity/client'
128
+
129
+ const client = createClient({
130
+ projectId: 'your-project-id',
131
+ dataset: 'your-dataset-name',
132
+ useCdn: false, // set to `true` to fetch from edge cache
133
+ apiVersion: '2022-01-12', // use current date (YYYY-MM-DD) to target the latest API version
134
+ })
135
+
136
+ const data = await client.fetch(`count(*)`)
137
+ console.log(`Number of documents: ${data}`)
138
+ ```
139
+
140
+ #### [CommonJS]
141
+
142
+ ```js
143
+ const {createClient} = require('@sanity/client')
144
+
145
+ const client = createClient({
146
+ projectId: 'your-project-id',
147
+ dataset: 'your-dataset-name',
148
+ useCdn: false, // set to `true` to fetch from edge cache
149
+ apiVersion: '2022-01-12', // use current date (YYYY-MM-DD) to target the latest API version
150
+ })
151
+
152
+ client
153
+ .fetch(`count(*)`)
154
+ .then((data) => console.log(`Number of documents: ${data}`))
155
+ .catch(console.error)
156
+ ```
157
+
158
+ #### [TypeScript]
159
+
160
+ ```ts
161
+ import {createClient, type ClientConfig} from '@sanity/client'
162
+
163
+ const config: ClientConfig = {
164
+ projectId: 'your-project-id',
165
+ dataset: 'your-dataset-name',
166
+ useCdn: false, // set to `true` to fetch from edge cache
167
+ apiVersion: '2022-01-12', // use current date (YYYY-MM-DD) to target the latest API version
168
+ }
169
+ const client = createClient(config)
170
+
171
+ const data = await client.fetch<number>(`count(*)`)
172
+ // data is typed as `number`
173
+ console.log(`Number of documents: ${data}`)
174
+ ```
175
+
176
+ We're currently exploring typed GROQ queries that are runtime safe, and will share more when we've landed on a solution we're satisifed with.
177
+ Until then you can achieve this using [Zod]:
178
+
179
+ ```ts
180
+ import {createClient} from '@sanity/client'
181
+ import {z} from 'zod'
182
+
183
+ const client = createClient({
184
+ projectId: 'your-project-id',
185
+ dataset: 'your-dataset-name',
186
+ useCdn: false, // set to `true` to fetch from edge cache
187
+ apiVersion: '2022-01-12', // use current date (YYYY-MM-DD) to target the latest API version
188
+ })
189
+
190
+ const schema = z.number()
191
+ const data = schema.parse(await client.fetch(`count(*)`))
192
+ // data is guaranteed to be `number`, or zod will throw an error
193
+ console.log(`Number of documents: ${data}`)
194
+ ```
195
+
196
+ Another alternative is [groqd].
197
+
198
+ #### [Bun]
199
+
200
+ ```bash
201
+ bun init
202
+ bun add @sanity/client
203
+ open index.ts
204
+ ```
205
+
206
+ ```ts
207
+ // index.ts
208
+ import {createClient} from '@sanity/client'
209
+
210
+ const client = createClient({
211
+ projectId: 'your-project-id',
212
+ dataset: 'your-dataset-name',
213
+ useCdn: false, // set to `true` to fetch from edge cache
214
+ apiVersion: '2022-01-12', // use current date (YYYY-MM-DD) to target the latest API version
215
+ })
216
+
217
+ const data = await client.fetch<number>(`count(*)`)
218
+
219
+ console.write(`Number of documents: ${data}`)
220
+ ```
221
+
222
+ ```bash
223
+ bun run index.ts
224
+ # Number of documents ${number}
225
+ ```
226
+
227
+ #### [Deno]
228
+
229
+ ```bash
230
+ deno init
231
+ open main.ts
232
+ ```
233
+
234
+ ```ts
235
+ // main.ts
236
+ import {createClient} from 'https://esm.sh/@sanity/client'
237
+
238
+ const client = createClient({
239
+ projectId: 'your-project-id',
240
+ dataset: 'your-dataset-name',
241
+ useCdn: false, // set to `true` to fetch from edge cache
242
+ apiVersion: '2022-01-12', // use current date (YYYY-MM-DD) to target the latest API version
243
+ })
244
+
245
+ const data = await client.fetch<number>(`count(*)`)
246
+
247
+ console.log(`Number of documents: ${data}`)
248
+ ```
249
+
250
+ ```bash
251
+ deno run --allow-net --allow-env main.ts
252
+ # Number of documents ${number}
253
+ ```
254
+
255
+ #### [Edge Runtime]
256
+
257
+ ```bash
258
+ npm install next
259
+ ```
260
+
261
+ ```ts
262
+ // pages/api/total.ts
263
+ import {createClient} from '@sanity/client'
264
+ import type {NextRequest} from 'next/server'
265
+
266
+ export const config = {
267
+ runtime: 'edge',
268
+ }
269
+
270
+ export default async function handler(req: NextRequest) {
271
+ const client = createClient({
272
+ projectId: 'your-project-id',
273
+ dataset: 'your-dataset-name',
274
+ useCdn: false, // set to `true` to fetch from edge cache
275
+ apiVersion: '2022-01-12', // use current date (YYYY-MM-DD) to target the latest API version
276
+ })
277
+
278
+ const count = await client.fetch<number>(`count(*)`)
279
+ return new Response(JSON.stringify({count}), {
280
+ status: 200,
281
+ headers: {
282
+ 'content-type': 'application/json',
283
+ },
284
+ })
285
+ }
286
+ ```
287
+
288
+ ```bash
289
+ npx next dev
290
+ # Open http://localhost:3000/api/total
291
+ # {"count": number}
292
+ ```
293
+
294
+ #### Browser ESM CDN
295
+
296
+ Using [esm.sh] you can either load the client using a `<script type="module">` tag:
297
+
298
+ ```html
299
+ <script type="module">
300
+ import {createClient} from 'https://esm.sh/@sanity/client'
301
+
302
+ const client = createClient({
303
+ projectId: 'your-project-id',
304
+ dataset: 'your-dataset-name',
305
+ useCdn: false, // set to `true` to fetch from edge cache
306
+ apiVersion: '2022-01-12', // use current date (YYYY-MM-DD) to target the latest API version
307
+ })
308
+
309
+ const data = await client.fetch(`count(*)`)
310
+ document.getElementById('results').innerText = `Number of documents: ${data}`
311
+ </script>
312
+ <div id="results"></div>
313
+ ```
314
+
315
+ Or from anywhere using a dynamic `import()`:
316
+
317
+ ```js
318
+ // You can run this snippet from your browser DevTools console.
319
+ // Super handy when you're quickly testing out queries.
320
+ const {createClient} = await import('https://esm.sh/@sanity/client')
321
+ const client = createClient({
322
+ projectId: 'your-project-id',
323
+ dataset: 'your-dataset-name',
324
+ useCdn: false, // set to `true` to fetch from edge cache
325
+ apiVersion: '2022-01-12', // use current date (YYYY-MM-DD) to target the latest API version
326
+ })
327
+
328
+ const data = await client.fetch(`count(*)`)
329
+ console.log(`Number of documents: ${data}`)
330
+ ```
331
+
332
+ #### [UMD][unpkg-dist]
333
+
334
+ Loading the UMD script creates a `SanityClient` global that have the same exports as `import * as SanityClient from '@sanity/client'`:
335
+
336
+ ```html
337
+ <script src="https://unpkg.com/@sanity/client"></script>
338
+ <!-- Unminified build for debugging -->
339
+ <!--<script src="https://unpkg.com/@sanity/client/umd/sanityClient.js"></script>-->
340
+ <script>
341
+ const {createClient} = SanityClient
342
+
343
+ const client = createClient({
344
+ projectId: 'your-project-id',
345
+ dataset: 'your-dataset-name',
346
+ useCdn: false, // set to `true` to fetch from edge cache
347
+ apiVersion: '2022-01-12', // use current date (YYYY-MM-DD) to target the latest API version
348
+ })
349
+
350
+ client.fetch(`count(*)`).then((data) => console.log(`Number of documents: ${data}`))
351
+ </script>
352
+ ```
353
+
354
+ The `require-unpkg` library lets you consume `npm` packages from `unpkg.com` similar to how `esm.sh` lets you `import()` anything:
355
+
356
+ ```html
357
+ <div id="results"></div>
358
+ <script src="https://unpkg.com/require-unpkg"></script>
359
+ <script>
360
+ ;(async () => {
361
+ // const {createClient} = await require('@sanity/client')
362
+ const [$, {createClient}] = await require(['jquery', '@sanity/client'])
363
+
364
+ const client = createClient({
365
+ projectId: 'your-project-id',
366
+ dataset: 'your-dataset-name',
367
+ useCdn: false, // set to `true` to fetch from edge cache
368
+ apiVersion: '2022-01-12', // use current date (YYYY-MM-DD) to target the latest API version
369
+ })
370
+
371
+ const data = await client.fetch(`count(*)`)
372
+ $('#results').text(`Number of documents: ${data}`)
373
+ })()
374
+ </script>
375
+ ```
376
+
377
+ ### Specifying API version
378
+
379
+ Sanity uses ISO dates (YYYY-MM-DD) in UTC timezone for versioning. The explanation for this can be found [in the documentation][api-versioning]
380
+
381
+ In general, unless you know what API version you want to use, you'll want to statically set it to today's UTC date when starting a new project. By doing this, you'll get all the latest bugfixes and features, while locking the API to prevent breaking changes.
382
+
383
+ **Note**: Do not be tempted to use a dynamic value for the `apiVersion`. The reason for setting a static value is to prevent unexpected, breaking changes.
384
+
385
+ In future versions, specifying an API version will be required. For now (to maintain backwards compatiblity) not specifying a version will trigger a deprecation warning and fall back to using `v1`.
386
+
387
+ ### Performing queries
388
+
389
+ ```js
390
+ const query = '*[_type == "bike" && seats >= $minSeats] {name, seats}'
391
+ const params = {minSeats: 2}
392
+
393
+ client.fetch(query, params).then((bikes) => {
394
+ console.log('Bikes with more than one seat:')
395
+ bikes.forEach((bike) => {
396
+ console.log(`${bike.name} (${bike.seats} seats)`)
397
+ })
398
+ })
399
+ ```
400
+
401
+ `client.fetch(query, params = {})`
402
+
403
+ Perform a query using the given parameters (if any).
404
+
405
+ ### Listening to queries
406
+
407
+ ```js
408
+ const query = '*[_type == "comment" && authorId != $ownerId]'
409
+ const params = {ownerId: 'bikeOwnerUserId'}
410
+
411
+ const subscription = client.listen(query, params).subscribe((update) => {
412
+ const comment = update.result
413
+ console.log(`${comment.author} commented: ${comment.text}`)
414
+ })
415
+
416
+ // to unsubscribe later on
417
+ subscription.unsubscribe()
418
+ ```
419
+
420
+ `client.listen(query, params = {}, options = {includeResult: true})`
421
+
422
+ Open a query that listens for updates on matched documents, using the given parameters (if any). The return value is an [RxJS Observable](https://rxjs.dev/guide/observable). When calling `.subscribe()` on the returned observable, a [subscription](https://rxjs.dev/api/index/class/Subscription) is returned, and this can be used to unsubscribe from the query later on by calling `subscription.unsubscribe()`
423
+
424
+ The update events which are emitted always contain `mutation`, which is an object containing the mutation which triggered the document to appear as part of the query.
425
+
426
+ By default, the emitted update event will also contain a `result` property, which contains the document with the mutation applied to it. In case of a delete mutation, this property will not be present, however. You can also tell the client not to return the document (to save bandwidth, or in cases where the mutation or the document ID is the only relevant factor) by setting the `includeResult` property to `false` in the options.
427
+
428
+ Likewise, you can also have the client return the document _before_ the mutation was applied, by setting `includePreviousRevision` to `true` in the options, which will include a `previous` property in each emitted object.
429
+
430
+ ### Fetch a single document
431
+
432
+ This will fetch a document from the [Doc endpoint](https://www.sanity.io/docs/http-doc). This endpoint cuts through any caching/indexing middleware that may involve delayed processing. As it is less scalable/performant than the other query mechanisms, it should be used sparingly. Performing a query is usually a better option.
433
+
434
+ ```js
435
+ client.getDocument('bike-123').then((bike) => {
436
+ console.log(`${bike.name} (${bike.seats} seats)`)
437
+ })
438
+ ```
439
+
440
+ ### Fetch multiple documents in one go
441
+
442
+ This will fetch multiple documents in one request from the [Doc endpoint](https://www.sanity.io/docs/http-doc). This endpoint cuts through any caching/indexing middleware that may involve delayed processing. As it is less scalable/performant than the other query mechanisms, it should be used sparingly. Performing a query is usually a better option.
443
+
444
+ ```js
445
+ client.getDocuments(['bike123', 'bike345']).then(([bike123, bike345]) => {
446
+ console.log(`Bike 123: ${bike123.name} (${bike123.seats} seats)`)
447
+ console.log(`Bike 345: ${bike345.name} (${bike345.seats} seats)`)
448
+ })
449
+ ```
450
+
451
+ Note: Unlike in the HTTP API, the order/position of documents is _preserved_ based on the original array of IDs. If any of the documents are missing, they will be replaced by a `null` entry in the returned array:
452
+
453
+ ```js
454
+ const ids = ['bike123', 'nonexistent-document', 'bike345']
455
+ client.getDocuments(ids).then((docs) => {
456
+ // the docs array will be:
457
+ // [{_id: 'bike123', ...}, null, {_id: 'bike345', ...}]
458
+ })
459
+ ```
460
+
461
+ ### Creating documents
462
+
463
+ ```js
464
+ const doc = {
465
+ _type: 'bike',
466
+ name: 'Sanity Tandem Extraordinaire',
467
+ seats: 2,
468
+ }
469
+
470
+ client.create(doc).then((res) => {
471
+ console.log(`Bike was created, document ID is ${res._id}`)
472
+ })
473
+ ```
474
+
475
+ `client.create(doc)`
476
+ `client.create(doc, mutationOptions)`
477
+
478
+ Create a document. Argument is a plain JS object representing the document. It must contain a `_type` attribute. It _may_ contain an `_id`. If an ID is not specified, it will automatically be created.
479
+
480
+ To create a draft document, prefix the document ID with `drafts.` - eg `_id: 'drafts.myDocumentId'`. To auto-generate a draft document ID, set `_id` to `drafts.` (nothing after the `.`).
481
+
482
+ ### Creating/replacing documents
483
+
484
+ ```js
485
+ const doc = {
486
+ _id: 'my-bike',
487
+ _type: 'bike',
488
+ name: 'Sanity Tandem Extraordinaire',
489
+ seats: 2,
490
+ }
491
+
492
+ client.createOrReplace(doc).then((res) => {
493
+ console.log(`Bike was created, document ID is ${res._id}`)
494
+ })
495
+ ```
496
+
497
+ `client.createOrReplace(doc)`
498
+ `client.createOrReplace(doc, mutationOptions)`
499
+
500
+ If you are not sure whether or not a document exists but want to overwrite it if it does, you can use the `createOrReplace()` method. When using this method, the document must contain an `_id` attribute.
501
+
502
+ ### Creating if not already present
503
+
504
+ ```js
505
+ const doc = {
506
+ _id: 'my-bike',
507
+ _type: 'bike',
508
+ name: 'Sanity Tandem Extraordinaire',
509
+ seats: 2,
510
+ }
511
+
512
+ client.createIfNotExists(doc).then((res) => {
513
+ console.log('Bike was created (or was already present)')
514
+ })
515
+ ```
516
+
517
+ `client.createIfNotExists(doc)`
518
+ `client.createIfNotExists(doc, mutationOptions)`
519
+
520
+ If you want to create a document if it does not already exist, but fall back without error if it does, you can use the `createIfNotExists()` method. When using this method, the document must contain an `_id` attribute.
521
+
522
+ ### Patch/update a document
523
+
524
+ ```js
525
+ client
526
+ .patch('bike-123') // Document ID to patch
527
+ .set({inStock: false}) // Shallow merge
528
+ .inc({numSold: 1}) // Increment field by count
529
+ .commit() // Perform the patch and return a promise
530
+ .then((updatedBike) => {
531
+ console.log('Hurray, the bike is updated! New document:')
532
+ console.log(updatedBike)
533
+ })
534
+ .catch((err) => {
535
+ console.error('Oh no, the update failed: ', err.message)
536
+ })
537
+ ```
538
+
539
+ Modify a document. `patch` takes a document ID. `set` merges the partialDoc with the stored document. `inc` increments the given field with the given numeric value. `commit` executes the given `patch`. Returns the updated object.
540
+
541
+ ```
542
+ client.patch()
543
+ [operations]
544
+ .commit(mutationOptions)
545
+ ```
546
+
547
+ ### Setting a field only if not already present
548
+
549
+ ```js
550
+ client.patch('bike-123').setIfMissing({title: 'Untitled bike'}).commit()
551
+ ```
552
+
553
+ ### Removing/unsetting fields
554
+
555
+ ```js
556
+ client.patch('bike-123').unset(['title', 'price']).commit()
557
+ ```
558
+
559
+ ### Incrementing/decrementing numbers
560
+
561
+ ```js
562
+ client
563
+ .patch('bike-123')
564
+ .inc({price: 88, numSales: 1}) // Increment `price` by 88, `numSales` by 1
565
+ .dec({inStock: 1}) // Decrement `inStock` by 1
566
+ .commit()
567
+ ```
568
+
569
+ ### Patch a document only if revision matches
570
+
571
+ You can use the `ifRevisionId(rev)` method to specify that you only want the patch to be applied if the stored document matches a given revision.
572
+
573
+ ```js
574
+ client
575
+ .patch('bike-123')
576
+ .ifRevisionId('previously-known-revision')
577
+ .set({title: 'Little Red Tricycle'})
578
+ .commit()
579
+ ```
580
+
581
+ ### Adding elements to an array
582
+
583
+ The patch operation `insert` takes a location (`before`, `after` or `replace`), a path selector and an array of elements to insert.
584
+
585
+ ```js
586
+ client
587
+ .patch('bike-123')
588
+ // Ensure that the `reviews` arrays exists before attempting to add items to it
589
+ .setIfMissing({reviews: []})
590
+ // Add the items after the last item in the array (append)
591
+ .insert('after', 'reviews[-1]', [{title: 'Great bike!', stars: 5}])
592
+ .commit({
593
+ // Adds a `_key` attribute to array items, unique within the array, to
594
+ // ensure it can be addressed uniquely in a real-time collaboration context
595
+ autoGenerateArrayKeys: true,
596
+ })
597
+ ```
598
+
599
+ ### Appending/prepending elements to an array
600
+
601
+ The operations of appending and prepending to an array are so common that they have been given their own methods for better readability:
602
+
603
+ ```js
604
+ client
605
+ .patch('bike-123')
606
+ .setIfMissing({reviews: []})
607
+ .append('reviews', [{title: 'Great bike!', stars: 5}])
608
+ .commit({autoGenerateArrayKeys: true})
609
+ ```
610
+
611
+ ### Deleting an element from an array
612
+
613
+ Each entry in the `unset` array can be either an attribute or a JSON path.
614
+
615
+ In this example, we remove the first review and the review with `_key: 'abc123'` from the `bike.reviews` array:
616
+
617
+ ```js
618
+ const reviewsToRemove = ['reviews[0]', 'reviews[_key=="abc123"]']
619
+ client.patch('bike-123').unset(reviewsToRemove).commit()
620
+ ```
621
+
622
+ ### Delete documents
623
+
624
+ A single document can be deleted by specifying a document ID:
625
+
626
+ `client.delete(docId)`
627
+ `client.delete(docId, mutationOptions)`
628
+
629
+ ```js
630
+ client
631
+ .delete('bike-123')
632
+ .then(() => {
633
+ console.log('Bike deleted')
634
+ })
635
+ .catch((err) => {
636
+ console.error('Delete failed: ', err.message)
637
+ })
638
+ ```
639
+
640
+ One or more documents can be deleted by specifying a GROQ query (and optionally, `params`):
641
+
642
+ `client.delete({ query: "GROQ query", params: { key: value } })`
643
+
644
+ ```js
645
+ // Without params
646
+ client
647
+ .delete({query: '*[_type == "bike"][0]'})
648
+ .then(() => {
649
+ console.log('The document matching *[_type == "bike"][0] was deleted')
650
+ })
651
+ .catch((err) => {
652
+ console.error('Delete failed: ', err.message)
653
+ })
654
+ ```
655
+
656
+ ```js
657
+ // With params
658
+ client
659
+ .delete({query: '*[_type == $type][0..1]', params: {type: 'bike'}})
660
+ .then(() => {
661
+ console.log('The documents matching *[_type == "bike"][0..1] was deleted')
662
+ })
663
+ .catch((err) => {
664
+ console.error('Delete failed: ', err.message)
665
+ })
666
+ ```
667
+
668
+ ### Multiple mutations in a transaction
669
+
670
+ ```js
671
+ const namePatch = client.patch('bike-310').set({name: 'A Bike To Go'})
672
+
673
+ client
674
+ .transaction()
675
+ .create({name: 'Sanity Tandem Extraordinaire', seats: 2})
676
+ .delete('bike-123')
677
+ .patch(namePatch)
678
+ .commit()
679
+ .then((res) => {
680
+ console.log('Whole lot of stuff just happened')
681
+ })
682
+ .catch((err) => {
683
+ console.error('Transaction failed: ', err.message)
684
+ })
685
+ ```
686
+
687
+ `client.transaction().create(doc).delete(docId).patch(patch).commit()`
688
+
689
+ Create a transaction to perform chained mutations.
690
+
691
+ ```js
692
+ client
693
+ .transaction()
694
+ .create({name: 'Sanity Tandem Extraordinaire', seats: 2})
695
+ .patch('bike-123', (p) => p.set({inStock: false}))
696
+ .commit()
697
+ .then((res) => {
698
+ console.log('Bike created and a different bike is updated')
699
+ })
700
+ .catch((err) => {
701
+ console.error('Transaction failed: ', err.message)
702
+ })
703
+ ```
704
+
705
+ `client.transaction().create(doc).patch(docId, p => p.set(partialDoc)).commit()`
706
+
707
+ A `patch` can be performed inline on a `transaction`.
708
+
709
+ ### Clientless patches & transactions
710
+
711
+ Transactions and patches can also be built outside the scope of a client:
712
+
713
+ ```js
714
+ import {createClient, Patch, Transaction} from '@sanity/client'
715
+ const client = createClient({
716
+ projectId: 'your-project-id',
717
+ dataset: 'bikeshop',
718
+ })
719
+
720
+ // Patches:
721
+ const patch = new Patch('<documentId>')
722
+ client.mutate(patch.inc({count: 1}).unset(['visits']))
723
+
724
+ // Transactions:
725
+ const transaction = new Transaction().create({_id: '123', name: 'FooBike'}).delete('someDocId')
726
+
727
+ client.mutate(transaction)
728
+ ```
729
+
730
+ `const patch = new Patch(docId)`
731
+
732
+ `const transaction = new Transaction()`
733
+
734
+ An important note on this approach is that you cannot call `commit()` on transactions or patches instantiated this way, instead you have to pass them to `client.mutate()`
735
+
736
+ ### Uploading assets
737
+
738
+ Assets can be uploaded using the `client.assets.upload(...)` method.
739
+
740
+ ```
741
+ client.assets.upload(type: 'file' | image', body: File | Blob | Buffer | NodeStream, options = {}): Promise<AssetDocument>
742
+ ```
743
+
744
+ 👉 Read more about [assets in Sanity](https://sanity.io/docs/assets)
745
+
746
+ #### Examples: Uploading assets from Node.js
747
+
748
+ ```js
749
+ // Upload a file from the file system
750
+ client.assets
751
+ .upload('file', fs.createReadStream('myFile.txt'), {filename: 'myFile.txt'})
752
+ .then((document) => {
753
+ console.log('The file was uploaded!', document)
754
+ })
755
+ .catch((error) => {
756
+ console.error('Upload failed:', error.message)
757
+ })
758
+ ```
759
+
760
+ ```js
761
+ // Upload an image file from the file system
762
+ client.assets
763
+ .upload('image', fs.createReadStream('myImage.jpg'), {filename: 'myImage.jpg'})
764
+ .then((document) => {
765
+ console.log('The image was uploaded!', document)
766
+ })
767
+ .catch((error) => {
768
+ console.error('Upload failed:', error.message)
769
+ })
770
+ ```
771
+
772
+ #### Examples: Uploading assets from the Browser
773
+
774
+ ```js
775
+ // Create a file with "foo" as its content
776
+ const file = new File(['foo'], 'foo.txt', {type: 'text/plain'})
777
+ // Upload it
778
+ client.assets
779
+ .upload('file', file)
780
+ .then((document) => {
781
+ console.log('The file was uploaded!', document)
782
+ })
783
+ .catch((error) => {
784
+ console.error('Upload failed:', error.message)
785
+ })
786
+ ```
787
+
788
+ ```js
789
+ // Draw something on a canvas and upload as image
790
+ const canvas = document.getElementById('someCanvas')
791
+ const ctx = canvas.getContext('2d')
792
+ ctx.fillStyle = '#f85040'
793
+ ctx.fillRect(0, 0, 50, 50)
794
+ ctx.fillStyle = '#fff'
795
+ ctx.font = '10px monospace'
796
+ ctx.fillText('Sanity', 8, 30)
797
+ canvas.toBlob(uploadImageBlob, 'image/png')
798
+
799
+ function uploadImageBlob(blob) {
800
+ client.assets
801
+ .upload('image', blob, {contentType: 'image/png', filename: 'someText.png'})
802
+ .then((document) => {
803
+ console.log('The image was uploaded!', document)
804
+ })
805
+ .catch((error) => {
806
+ console.error('Upload failed:', error.message)
807
+ })
808
+ }
809
+ ```
810
+
811
+ #### Examples: Specify image metadata to extract
812
+
813
+ ```js
814
+ // Extract palette of colors as well as GPS location from exif
815
+ client.assets
816
+ .upload('image', someFile, {extract: ['palette', 'location']})
817
+ .then((document) => {
818
+ console.log('The file was uploaded!', document)
819
+ })
820
+ .catch((error) => {
821
+ console.error('Upload failed:', error.message)
822
+ })
823
+ ```
824
+
825
+ ### Deleting an asset
826
+
827
+ Deleting an asset document will also trigger deletion of the actual asset.
828
+
829
+ ```
830
+ client.delete(assetDocumentId: string): Promise
831
+ ```
832
+
833
+ ```js
834
+ client.delete('image-abc123_someAssetId-500x500-png').then((result) => {
835
+ console.log('deleted imageAsset', result)
836
+ })
837
+ ```
838
+
839
+ ### Mutation options
840
+
841
+ The following options are available for mutations, and can be applied either as the second argument to `create()`, `createOrReplace`, `createIfNotExist`, `delete()` and `mutate()` - or as an argument to the `commit()` method on patches and transactions.
842
+
843
+ - `visibility` (`'sync'|'async'|'deferred'`) - default `'sync'`
844
+ - `sync`: request will not return until the requested changes are visible to subsequent queries.
845
+ - `async`: request will return immediately when the changes have been committed, but it might still be a second or more until changes are reflected in a query. Unless you are immediately re-querying for something that includes the mutated data, this is the preferred choice.
846
+ - `deferred`: fastest way to write - bypasses real-time indexing completely, and should be used in cases where you are bulk importing/mutating a large number of documents and don't need to see that data in a query for tens of seconds.
847
+ - `dryRun` (`true|false`) - default `false`. If true, the mutation will be a dry run - the response will be identical to the one returned had this property been omitted or false (including error responses) but no documents will be affected.
848
+ - `autoGenerateArrayKeys` (`true|false`) - default `false`. If true, the mutation API will automatically add `_key` attributes to objects in arrays that is missing them. This makes array operations more robust by having a unique key within the array available for selections, which helps prevent race conditions in real-time, collaborative editing.
849
+
850
+ ### Aborting a request
851
+
852
+ Requests can be aborted (or cancelled) in two ways:
853
+
854
+ #### 1. Abort a request by passing an [AbortSignal] with the request options
855
+
856
+ Sanity Client supports the [AbortController] API and supports receiving an abort signal that can be used to cancel the request. Here's an example that will abort the request if it takes more than 200ms to complete:
857
+
858
+ ```js
859
+ const abortController = new AbortController()
860
+
861
+ // note the lack of await here
862
+ const request = getClient().fetch('*[_type == "movie"]', {}, {signal: abortController.signal})
863
+
864
+ // this will abort the request after 200ms
865
+ setTimeout(() => abortController.abort(), 200)
866
+
867
+ try {
868
+ const response = await request
869
+ //…
870
+ } catch (error) {
871
+ if (error.name === 'AbortError') {
872
+ console.log('Request was aborted')
873
+ } else {
874
+ // rethrow in case of other errors
875
+ throw error
876
+ }
877
+ }
878
+ ```
879
+
880
+ #### 2. Cancel a request by unsubscribing from the Observable
881
+
882
+ When using the Observable API (e.g. `client.observable.fetch()`), you can cancel the request by simply `unsubscribe` from the returned observable:
883
+
884
+ ```js
885
+ const subscription = client.observable.fetch('*[_type == "movie"]').subscribe((result) => {
886
+ /* do something with the result */
887
+ })
888
+
889
+ // this will cancel the request
890
+ subscription.unsubscribe()
891
+ ```
892
+
893
+ ### Get client configuration
894
+
895
+ ```js
896
+ const config = client.config()
897
+ console.log(config.dataset)
898
+ ```
899
+
900
+ `client.config()`
901
+
902
+ Get client configuration.
903
+
904
+ ### Set client configuration
905
+
906
+ ```js
907
+ client.config({dataset: 'newDataset'})
908
+ ```
909
+
910
+ `client.config(options)`
911
+
912
+ Set client configuration. Required options are `projectId` and `dataset`.
913
+
914
+ ## Release new version
915
+
916
+ Run ["CI & Release" workflow](https://github.com/sanity-io/client/actions/workflows/ci.yml).
917
+ Make sure to select the main branch and check "Release new version".
918
+
919
+ Semantic release will only release on configured branches, so it is safe to run release on any branch.
920
+
921
+ ## License
922
+
923
+ MIT © [Sanity.io](https://www.sanity.io/)
924
+
925
+ # Migrate
926
+
927
+ ## From `v4`
928
+
929
+ ### No longer shipping `ES5`
930
+
931
+ The target is changed to [modern browsers] that supports `ES6` `class`, `{...rest}` syntax and more. You may need to update your bundler to a recent major version. Or you could configure your bundler to transpile `@sanity/client`, and `get-it`, which is the engine that powers `@sanity/client` and uses the same output target.
932
+
933
+ ### Node.js `v12` no longer supported
934
+
935
+ Upgrade to the [LTS release, or one of the Maintenance releases](https://github.com/nodejs/release#release-schedule).
936
+
937
+ ### The `default` export is replaced with the named export `createClient`
938
+
939
+ Before:
940
+
941
+ ```ts
942
+ import createClient from '@sanity/client'
943
+ const client = createClient()
944
+ ```
945
+
946
+ ```ts
947
+ import SanityClient from '@sanity/client'
948
+ const client = new SanityClient()
949
+ ```
950
+
951
+ After:
952
+
953
+ ```ts
954
+ import {createClient} from '@sanity/client'
955
+ const client = createClient()
956
+ ```
957
+
958
+ ### `client.assets.delete` is removed
959
+
960
+ Before:
961
+
962
+ ```ts
963
+ client.assets.delete('image', 'abc123_foobar-123x123-png')
964
+ client.assets.delete('image', 'image-abc123_foobar-123x123-png')
965
+ client.assets.delete({_id: 'image-abc123_foobar-123x123-png'})
966
+ ```
967
+
968
+ After:
969
+
970
+ ```ts
971
+ client.delete('image-abc123_foobar-123x123-png')
972
+ ```
973
+
974
+ ### `client.assets.getImageUrl` is removed, replace with [`@sanity/image-url`](https://github.com/sanity-io/image-url)
975
+
976
+ Before:
977
+
978
+ ```ts
979
+ import createClient from '@sanity/client'
980
+ const client = createClient({projectId: 'abc123', dataset: 'foo'})
981
+
982
+ client.assets.getImageUrl('image-abc123_foobar-123x123-png')
983
+ client.assets.getImageUrl('image-abc123_foobar-123x123-png', {auto: 'format'})
984
+ client.assets.getImageUrl({_ref: 'image-abc123_foobar-123x123-png'})
985
+ client.assets.getImageUrl({_ref: 'image-abc123_foobar-123x123-png'}, {auto: 'format'})
986
+ ```
987
+
988
+ After:
989
+
990
+ ```bash
991
+ npm install @sanity/image-url
992
+ ```
993
+
994
+ ```ts
995
+ import imageUrlBuilder from '@sanity/image-url'
996
+ const builder = imageUrlBuilder({projectId: 'abc123', dataset: 'foo'})
997
+ const urlFor = (source) => builder.image(source)
998
+
999
+ urlFor('image-abc123_foobar-123x123-png').url()
1000
+ urlFor('image-abc123_foobar-123x123-png').auto('format').url()
1001
+ urlFor({_ref: 'image-abc123_foobar-123x123-png'}).url()
1002
+ urlFor({_ref: 'image-abc123_foobar-123x123-png'}).auto('format').url()
1003
+ ```
1004
+
1005
+ ### `SanityClient` static properties moved to named exports
1006
+
1007
+ Before:
1008
+
1009
+ ```ts
1010
+ import SanityClient from '@sanity/client'
1011
+
1012
+ const {Patch, Transaction, ClientError, ServerError, requester} = SanityClient
1013
+ ```
1014
+
1015
+ After:
1016
+
1017
+ ```ts
1018
+ import {Patch, Transaction, ClientError, ServerError, requester} from '@sanity/client'
1019
+ ```
1020
+
1021
+ ### `client.clientConfig` is removed, replace with `client.config()`
1022
+
1023
+ Before:
1024
+
1025
+ ```ts
1026
+ import createClient from '@sanity/client'
1027
+ const client = createClient()
1028
+
1029
+ console.log(client.clientConfig.projectId)
1030
+ ```
1031
+
1032
+ After:
1033
+
1034
+ ```ts
1035
+ import {createClient} from '@sanity/client'
1036
+ const client = createClient()
1037
+
1038
+ console.log(client.config().projectId)
1039
+ ```
1040
+
1041
+ ### `client.isPromiseAPI()` is removed, replace with an `instanceof` check
1042
+
1043
+ Before:
1044
+
1045
+ ```ts
1046
+ import createClient from '@sanity/client'
1047
+ const client = createClient()
1048
+
1049
+ console.log(client.isPromiseAPI())
1050
+ console.log(client.clientConfig.isPromiseAPI)
1051
+ console.log(client.config().isPromiseAPI)
1052
+ ```
1053
+
1054
+ After:
1055
+
1056
+ ```ts
1057
+ import {createClient, SanityClient} from '@sanity/client'
1058
+ const client = createClient()
1059
+
1060
+ console.log(client instanceof SanityClient)
1061
+ ```
1062
+
1063
+ ### `client.observable.isObservableAPI()` is removed, replace with an `instanceof` check
1064
+
1065
+ Before:
1066
+
1067
+ ```ts
1068
+ import createClient from '@sanity/client'
1069
+ const client = createClient()
1070
+
1071
+ console.log(client.observable.isObservableAPI())
1072
+ ```
1073
+
1074
+ After:
1075
+
1076
+ ```ts
1077
+ import {createClient, ObservableSanityClient} from '@sanity/client'
1078
+ const client = createClient()
1079
+
1080
+ console.log(client.observable instanceof ObservableSanityClient)
1081
+ ```
1082
+
1083
+ ### `client._requestObservable` is removed, replace with `client.observable.request`
1084
+
1085
+ Before:
1086
+
1087
+ ```ts
1088
+ import createClient from '@sanity/client'
1089
+ const client = createClient()
1090
+
1091
+ client._requestObservable({uri: '/ping'}).subscribe()
1092
+ ```
1093
+
1094
+ After:
1095
+
1096
+ ```ts
1097
+ import {createClient} from '@sanity/client'
1098
+ const client = createClient()
1099
+
1100
+ client.observable.request({uri: '/ping'}).subscribe()
1101
+ ```
1102
+
1103
+ ### `client._dataRequest` is removed, replace with `client.dataRequest`
1104
+
1105
+ Before:
1106
+
1107
+ ```ts
1108
+ import createClient from '@sanity/client'
1109
+ const client = createClient()
1110
+
1111
+ client._dataRequest(endpoint, body, options)
1112
+ ```
1113
+
1114
+ After:
1115
+
1116
+ ```ts
1117
+ import {createClient} from '@sanity/client'
1118
+ const client = createClient()
1119
+
1120
+ client.dataRequest(endpoint, body, options)
1121
+ ```
1122
+
1123
+ ### `client._create_` is removed, replace with one of `client.create`, `client.createIfNotExists` or `client.createOrReplace`
1124
+
1125
+ Before:
1126
+
1127
+ ```ts
1128
+ import createClient from '@sanity/client'
1129
+ const client = createClient()
1130
+
1131
+ client._create(doc, 'create', options)
1132
+ client._create(doc, 'createIfNotExists', options)
1133
+ client._create(doc, 'createOrReplace', options)
1134
+ ```
1135
+
1136
+ After:
1137
+
1138
+ ```ts
1139
+ import {createClient} from '@sanity/client'
1140
+ const client = createClient()
1141
+
1142
+ client.create(doc, options)
1143
+ client.createIfNotExists(doc, options)
1144
+ client.createOrReplace(doc, options)
1145
+ ```
1146
+
1147
+ ### `client.patch.replace` is removed, replace with `client.createOrReplace`
1148
+
1149
+ Before:
1150
+
1151
+ ```ts
1152
+ import createClient from '@sanity/client'
1153
+ const client = createClient()
1154
+
1155
+ client.patch('tropic-hab').replace({name: 'Tropical Habanero', ingredients: []}).commit()
1156
+ ```
1157
+
1158
+ After:
1159
+
1160
+ ```ts
1161
+ import {createClient} from '@sanity/client'
1162
+ const client = createClient()
1163
+
1164
+ client.createOrReplace({
1165
+ _id: 'tropic-hab',
1166
+ _type: 'hotsauce',
1167
+ name: 'Tropical Habanero',
1168
+ ingredients: [],
1169
+ })
1170
+ ```
1171
+
1172
+ ### `client.auth` is removed, replace with `client.request`
1173
+
1174
+ Before:
1175
+
1176
+ ```ts
1177
+ import createClient from '@sanity/client'
1178
+ const client = createClient()
1179
+
1180
+ /**
1181
+ * Fetch available login providers
1182
+ */
1183
+ const loginProviders = await client.auth.getLoginProviders()
1184
+ /**
1185
+ * Revoke the configured session/token
1186
+ */
1187
+ await client.auth.logout()
1188
+ ```
1189
+
1190
+ After:
1191
+
1192
+ ```ts
1193
+ import {createclient, type AuthProviderResponse} from '@sanity/client'
1194
+ const client = createClient()
1195
+
1196
+ /**
1197
+ * Fetch available login providers
1198
+ */
1199
+ const loginProviders = await client.request<AuthProviderResponse>({uri: '/auth/providers'})
1200
+ /**
1201
+ * Revoke the configured session/token
1202
+ */
1203
+ await client.request<void>({uri: '/auth/logout', method: 'POST'})
1204
+ ```
1205
+
1206
+ [modern browsers]: https://browsersl.ist/#q=%3E+0.2%25+and+supports+es6-module+and+supports+es6-module-dynamic-import+and+not+dead+and+not+IE+11
1207
+ [Deno]: https://deno.land/
1208
+ [Edge Runtime]: https://edge-runtime.vercel.sh/
1209
+ [Bun]: https://bun.sh/
1210
+ [gzip-badge]: https://img.shields.io/bundlephobia/minzip/@sanity/client?label=gzip%20size&style=flat-square
1211
+ [size-badge]: https://img.shields.io/bundlephobia/min/@sanity/client?label=size&style=flat-square
1212
+ [unpkg-dist]: https://unpkg.com/@sanity/client/umd/
1213
+ [bundlephobia]: https://bundlephobia.com/package/@sanity/client
1214
+ [esm.sh]: https://esm.sh
1215
+ [Node.js]: https://nodejs.org/en/
1216
+ [Content Lake]: https://www.sanity.io/docs/datastore
1217
+ [npm]: https://npmjs.com
1218
+ [api-cdn]: https://www.sanity.io/docs/api-cdn
1219
+ [CommonJS]: https://nodejs.org/api/modules.html#modules-commonjs-modules
1220
+ [TypeScript]: https://www.typescriptlang.org/
1221
+ [api-versioning]: http://sanity.io/docs/api-versioning
1222
+ [zod]: https://zod.dev/
1223
+ [groqd]: https://github.com/FormidableLabs/groqd#readme
1224
+ [AbortSignal]: https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal
1225
+ [AbortController]: https://developer.mozilla.org/en-US/docs/Web/API/AbortController