Giter Club home page Giter Club logo

client's Introduction

@sanity/client

npm stat npm version gzip size size

JavaScript client for Sanity. Works in modern browsers, as well as runtimes like Node.js, Bun, Deno, and Edge Runtime

QuickStart

Install the client with a package manager:

npm install @sanity/client

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.

// sanity.js
import {createClient} from '@sanity/client'
// Import using ESM URL imports in environments that supports it:
// import {createClient} from 'https://esm.sh/@sanity/client'

export const client = createClient({
  projectId: 'your-project-id',
  dataset: 'your-dataset-name',
  useCdn: true, // set to `false` to bypass the edge cache
  apiVersion: '2023-05-03', // use current date (YYYY-MM-DD) to target the latest API version
  // token: process.env.SANITY_SECRET_TOKEN // Only if you want to update content with the client
})

// uses GROQ to query content: https://www.sanity.io/docs/groq
export async function getPosts() {
  const posts = await client.fetch('*[_type == "post"]')
  return posts
}

export async function createPost(post: Post) {
  const result = client.create(post)
  return result
}

export async function updateDocumentTitle(_id, title) {
  const result = client.patch(_id).set({title})
  return result
}

Table of contents

Requirements

Sanity Client transpiles syntax for modern browsers. The JavaScript runtime must support ES6 features such as class, rest parameters, spread syntax and more. Most modern web frameworks, browsers, and developer tooling supports ES6 today.

For legacy ES5 environments we recommend v4.

Installation

The client can be installed from npm:

npm install @sanity/client

# Alternative package managers
yarn add @sanity/client
pnpm install @sanity/client

API

Creating a client instance

const client = createClient(options)

Initializes a new Sanity Client. Required options are projectId, dataset, and apiVersion. We encourage setting useCdn to either true or false. The default is true. If you're not sure which option to choose we recommend starting with true and revise later if you find that you require uncached content. Our awesome Slack community can help guide you on how to avoid stale data tailored to your tech stack and architecture.

import {createClient} from '@sanity/client'

const client = createClient({
  projectId: 'your-project-id',
  dataset: 'your-dataset-name',
  useCdn: true, // set to `false` to bypass the edge cache
  apiVersion: '2023-05-03', // use current date (YYYY-MM-DD) to target the latest API version
})

const data = await client.fetch(`count(*)`)
console.log(`Number of documents: ${data}`)
const {createClient} = require('@sanity/client')

const client = createClient({
  projectId: 'your-project-id',
  dataset: 'your-dataset-name',
  useCdn: true, // set to `false` to bypass the edge cache
  apiVersion: '2023-05-03', // use current date (YYYY-MM-DD) to target the latest API version
})

client
  .fetch(`count(*)`)
  .then((data) => console.log(`Number of documents: ${data}`))
  .catch(console.error)
import {createClient, type ClientConfig} from '@sanity/client'

const config: ClientConfig = {
  projectId: 'your-project-id',
  dataset: 'your-dataset-name',
  useCdn: true, // set to `false` to bypass the edge cache
  apiVersion: '2023-05-03', // use current date (YYYY-MM-DD) to target the latest API version
}
const client = createClient(config)

const data = await client.fetch<number>(`count(*)`)
// data is typed as `number`
console.log(`Number of documents: ${data}`)

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. Until then you can achieve this using Zod:

import {createClient} from '@sanity/client'
import {z} from 'zod'

const client = createClient({
  projectId: 'your-project-id',
  dataset: 'your-dataset-name',
  useCdn: true, // set to `false` to bypass the edge cache
  apiVersion: '2023-05-03', // use current date (YYYY-MM-DD) to target the latest API version
})

const schema = z.number()
const data = schema.parse(await client.fetch(`count(*)`))
// data is guaranteed to be `number`, or zod will throw an error
console.log(`Number of documents: ${data}`)

Another alternative is groqd.

import {createClient} from '@sanity/client'

const client = createClient({
  projectId: 'your-project-id',
  dataset: 'your-dataset-name',
  useCdn: true, // set to `false` to bypass the edge cache
  apiVersion: '2023-05-03', // use current date (YYYY-MM-DD) to target the latest API version
})

export default async function ReactServerComponent() {
  const data = await client.fetch<number>(
    `count(*[_type == "page"])`,
    {},
    {
      // You can set any of the `cache` and `next` options as you would on a standard `fetch` call
      cache: 'force-cache',
      next: {tags: ['pages']},
    },
  )

  return <h1>Number of pages: {data}</h1>
}

The cache and next options are documented in the Next.js documentation. Since request memoization is supported it's unnecessary to use the React.cache API. To opt-out of memoization, set the signal property:

const {signal} = new AbortController()
// By passing `signal` this request will not be memoized and `now()` will execute for every React Server Component that runs this query
const data = await client.fetch<number>(`{"dynamic": now()}`, {}, {signal})
bun init
bun add @sanity/client
open index.ts
// index.ts
import {createClient} from '@sanity/client'

const client = createClient({
  projectId: 'your-project-id',
  dataset: 'your-dataset-name',
  useCdn: true, // set to `false` to bypass the edge cache
  apiVersion: '2023-05-03', // use current date (YYYY-MM-DD) to target the latest API version
})

const data = await client.fetch<number>(`count(*)`)

console.write(`Number of documents: ${data}`)
bun run index.ts
# Number of documents ${number}
deno init
open main.ts
// main.ts
import {createClient} from 'https://esm.sh/@sanity/client'

const client = createClient({
  projectId: 'your-project-id',
  dataset: 'your-dataset-name',
  useCdn: true, // set to `false` to bypass the edge cache
  apiVersion: '2023-05-03', // use current date (YYYY-MM-DD) to target the latest API version
})

const data = await client.fetch<number>(`count(*)`)

console.log(`Number of documents: ${data}`)
deno run --allow-net --allow-env main.ts
# Number of documents ${number}
npm install next
// pages/api/total.ts
import {createClient} from '@sanity/client'
import type {NextRequest} from 'next/server'

export const config = {
  runtime: 'edge',
}

export default async function handler(req: NextRequest) {
  const client = createClient({
    projectId: 'your-project-id',
    dataset: 'your-dataset-name',
    useCdn: true, // set to `false` to bypass the edge cache
    apiVersion: '2023-05-03', // use current date (YYYY-MM-DD) to target the latest API version
  })

  const count = await client.fetch<number>(`count(*)`)
  return new Response(JSON.stringify({count}), {
    status: 200,
    headers: {
      'content-type': 'application/json',
    },
  })
}
npx next dev
# Open http://localhost:3000/api/total
# {"count": number}

Browser ESM CDN

Using esm.sh you can either load the client using a <script type="module"> tag:

<script type="module">
  import {createClient} from 'https://esm.sh/@sanity/client'

  const client = createClient({
    projectId: 'your-project-id',
    dataset: 'your-dataset-name',
    useCdn: true, // set to `false` to bypass the edge cache
    apiVersion: '2023-05-03', // use current date (YYYY-MM-DD) to target the latest API version
  })

  const data = await client.fetch(`count(*)`)
  document.getElementById('results').innerText = `Number of documents: ${data}`
</script>
<div id="results"></div>

Or from anywhere using a dynamic import():

// You can run this snippet from your browser DevTools console.
// Super handy when you're quickly testing out queries.
const {createClient} = await import('https://esm.sh/@sanity/client')
const client = createClient({
  projectId: 'your-project-id',
  dataset: 'your-dataset-name',
  useCdn: true, // set to `false` to bypass the edge cache
  apiVersion: '2023-05-03', // use current date (YYYY-MM-DD) to target the latest API version
})

const data = await client.fetch(`count(*)`)
console.log(`Number of documents: ${data}`)

Loading the UMD script creates a SanityClient global that have the same exports as import * as SanityClient from '@sanity/client':

<script src="https://unpkg.com/@sanity/client"></script>
<!-- Unminified build for debugging -->
<!--<script src="https://unpkg.com/@sanity/client/umd/sanityClient.js"></script>-->
<script>
  const {createClient} = SanityClient

  const client = createClient({
    projectId: 'your-project-id',
    dataset: 'your-dataset-name',
    useCdn: true, // set to `false` to bypass the edge cache
    apiVersion: '2023-05-03', // use current date (YYYY-MM-DD) to target the latest API version
  })

  client.fetch(`count(*)`).then((data) => console.log(`Number of documents: ${data}`))
</script>

The require-unpkg library lets you consume npm packages from unpkg.com similar to how esm.sh lets you import() anything:

<div id="results"></div>
<script src="https://unpkg.com/require-unpkg"></script>
<script>
  ;(async () => {
    // const {createClient} = await require('@sanity/client')
    const [$, {createClient}] = await require(['jquery', '@sanity/client'])

    const client = createClient({
      projectId: 'your-project-id',
      dataset: 'your-dataset-name',
      useCdn: true, // set to `false` to bypass the edge cache
      apiVersion: '2023-05-03', // use current date (YYYY-MM-DD) to target the latest API version
    })

    const data = await client.fetch(`count(*)`)
    $('#results').text(`Number of documents: ${data}`)
  })()
</script>

Specifying API version

Sanity uses ISO dates (YYYY-MM-DD) in UTC timezone for versioning. The explanation for this can be found in the documentation

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.

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.

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.

Request tags

Request tags are values assigned to API and CDN requests that can be used to filter and aggregate log data within request logs from your Sanity Content Lake.

Sanity Client has out-of-the-box support for tagging every API and CDN request on two levels:

  • Globally: Using the requestTagPrefix client configuration parameter
  • Per Request: Pass the tag option to the SDK’s Request method.

The following example will result in a query with tag=website.landing-page:

const client = createClient({
  projectId: '<project>',
  dataset: '<dataset>',
  useCdn: true,
  apiVersion: '2024-01-24',
  requestTagPrefix: 'website', // Added to every request
})

const posts = await client.fetch('*[_type == "post"]', {
  tag: `index-page`, // Appended to requestTagPrefix for this individual request
})

Performing queries

const query = '*[_type == "bike" && seats >= $minSeats] {name, seats}'
const params = {minSeats: 2}

client.fetch(query, params).then((bikes) => {
  console.log('Bikes with more than one seat:')
  bikes.forEach((bike) => {
    console.log(`${bike.name} (${bike.seats} seats)`)
  })
})

client.fetch(query, params = {})

Perform a query using the given parameters (if any).

Using perspectives

The perspective option can be used to specify special filtering behavior for queries. The default value is raw, which means no special filtering is applied, while published and previewDrafts can be used to optimize for specific use cases.

published

Useful for when you want to be sure that draft documents are not returned in production. Pairs well with private datasets.

With a dataset that looks like this:

[
  {
    "_type": "author",
    "_id": "ecfef291-60f0-4609-bbfc-263d11a48c43",
    "name": "George Martin"
  },
  {
    "_type": "author",
    "_id": "drafts.ecfef291-60f0-4609-bbfc-263d11a48c43",
    "name": "George R.R. Martin"
  },
  {
    "_type": "author",
    "_id": "drafts.f4898efe-92c4-4dc0-9c8c-f7480aef17e2",
    "name": "Stephen King"
  }
]

And a query like this:

import {createClient} from '@sanity/client'

const client = createClient({
  ...config,
  useCdn: true, // set to `false` to bypass the edge cache
  perspective: 'published',
})

const authors = await client.fetch('*[_type == "author"]')

Then authors will only contain documents that don't have a drafts. prefix in their _id, in this case just "George Martin":

[
  {
    "_type": "author",
    "_id": "ecfef291-60f0-4609-bbfc-263d11a48c43",
    "name": "George Martin"
  }
]

previewDrafts

Designed to help answer the question "What is our app going to look like after all the draft documents are published?".

Given a dataset like this:

[
  {
    "_type": "author",
    "_id": "ecfef291-60f0-4609-bbfc-263d11a48c43",
    "name": "George Martin"
  },
  {
    "_type": "author",
    "_id": "drafts.ecfef291-60f0-4609-bbfc-263d11a48c43",
    "name": "George R.R. Martin"
  },
  {
    "_type": "author",
    "_id": "drafts.f4898efe-92c4-4dc0-9c8c-f7480aef17e2",
    "name": "Stephen King"
  },
  {
    "_type": "author",
    "_id": "6b3792d2-a9e8-4c79-9982-c7e89f2d1e75",
    "name": "Terry Pratchett"
  }
]

And a query like this:

import {createClient} from '@sanity/client'

const client = createClient({
  ...config,
  useCdn: false, // the `previewDrafts` perspective requires this to be `false`
  perspective: 'previewDrafts',
})

const authors = await client.fetch('*[_type == "author"]')

Then authors will look like this. Note that the result dedupes documents with a preference for the draft version:

[
  {
    "_type": "author",
    "_id": "ecfef291-60f0-4609-bbfc-263d11a48c43",
    "_originalId": "drafts.ecfef291-60f0-4609-bbfc-263d11a48c43",
    "name": "George R.R. Martin"
  },
  {
    "_type": "author",
    "_id": "f4898efe-92c4-4dc0-9c8c-f7480aef17e2",
    "_originalId": "drafts.f4898efe-92c4-4dc0-9c8c-f7480aef17e2",
    "name": "Stephen King"
  },
  {
    "_type": "author",
    "_id": "6b3792d2-a9e8-4c79-9982-c7e89f2d1e75",
    "_originalId": "6b3792d2-a9e8-4c79-9982-c7e89f2d1e75",
    "name": "Terry Pratchett"
  }
]

Since the query simulates what the result will be after publishing the drafts, the _id doesn't contain the drafts. prefix. If you want to check if a document is a draft or not you can use the _originalId field, which is only available when using the previewDrafts perspective.

const authors = await client.fetch(`*[_type == "author"]{..., "status": select(
  _originalId in path("drafts.**") => "draft",
  "published"
)}`)

Which changes the result to be:

[
  {
    "_type": "author",
    "_id": "ecfef291-60f0-4609-bbfc-263d11a48c43",
    "_originalId": "drafts.ecfef291-60f0-4609-bbfc-263d11a48c43",
    "name": "George R.R. Martin",
    "status": "draft"
  },
  {
    "_type": "author",
    "_id": "f4898efe-92c4-4dc0-9c8c-f7480aef17e2",
    "_originalId": "f4898efe-92c4-4dc0-9c8c-f7480aef17e2",
    "name": "Stephen King",
    "status": "published"
  }
]

Fetching Content Source Maps

Content Source Maps annotate fragments in your query results with metadata about its origin: the field, document, and dataset it originated from.

Important

Content Source Maps are supported in the Content Lake API versions 2021-03-25 and later.

Before diving in, review the Content Source Maps introduction and keep the Content Source Maps reference within reach for a quick lookup.

Enabling Content Source Maps is a two-step process:

  1. Update your client configuration with resultSourceMap.

    import {createClient} from '@sanity/client'
    
    const client = createClient({
      projectId: 'your-project-id',
      dataset: 'your-dataset-name',
      useCdn: true, // set to `false` to bypass the edge cache
      apiVersion: '2021-03-25', // use current date (YYYY-MM-DD) to target the latest API version
      resultSourceMap: true, // tells the API to start sending source maps, if available
    })
  2. On client.fetch calls add {filterResponse: false} to return the full response on queries.

    // Before
    // const result = await client.fetch(query, params)
    
    // After adding `filterResponse: false`
    const {result, resultSourceMap} = await client.fetch(query, params, {filterResponse: false})
    // Build something cool with the source map
    console.log(resultSourceMap)

If your apiVersion is 2021-03-25 or later, the resultSourceMap property will always exist in the response after enabling it. If there is no source map, resultSourceMap is an empty object. This is the corresponding TypeScript definition:

import type {ContentSourceMapping} from '@sanity/client'

const {result, resultSourceMap} = await client.fetch(query, params, {filterResponse: false})

function useContentSourceMap(resultSourceMap: ContentSourceMapping): unknown {
  // Sky's the limit
}

useContentSourceMap(resultSourceMap)

Using Visual editing with steganography

A turnkey integration with Visual editing is available in [@sanity/client], with additional utils available on [@sanity/client/stega]. It creates edit intent links for all the string values in your query result, using steganography under the hood. The code that handles stega is lazy loaded on demand when client.fetch is called, if client.config().stega.enabled is true.

import {createClient} from '@sanity/client'

const client = createClient({
  // ...base config options
  stega: {
    // If you use Vercel Visual Editing, we recommend enabling it for Preview deployments
    enabled: process.env.VERCEL_ENV === 'preview',
    // Required: Set it to the relative or absolute URL of your Sanity Studio instance
    studioUrl: '/studio', // or 'https://your-project-name.sanity.studio'
    // To resolve Cross Dataset References, pass a function returning a URL
    studioUrl: (sourceDocument: ContentSourceMapDocument | ContentSourceMapRemoteDocument) => {
      // If `sourceDocument` has a projectId and a dataset, then it's a Cross Dataset Reference
      if (source._projectId && source._dataset) {
        return 'https://acme-global.sanity.studio'
      }
      return 'https://acme-store.sanity.studio'
    },
    // If your Studio has Workspaces: https://www.sanity.io/docs/workspaces
    // and if your Cross Dataset References are available in a workspace, you can return an object to let the client set up the URL
    studioUrl: (sourceDocument) => {
      // This organization has a single studio with everything organized in workspaces
      const baseUrl = 'https://acme.sanity.studio'
      // If `sourceDocument` has a projectId and a dataset, then it's a Cross Dataset Reference
      if (source._projectId && source._dataset) {
        return {baseUrl, workspace: 'global'}
      }
      return {baseUrl, workspace: 'store'}
    },

    // Optional, to control which fields have stega payloads
    filter: (props) => {
      const {resultPath, sourcePath, sourceDocument, value} = props
      if (sourcePath[0] === 'externalurl') {
        return false
      }
      // The default behavior is packaged into `filterDefault`, allowing you to enable encoding fields that are skipped by default
      return props.filterDefault(props)
    },

    // Optional, to log what's encoded and what isn't
    // logger: console,
  },
})

// Disable on demand
client.config({stega: {enabled: false}})

// New client with different stega settings
const debugClient = client.withConfig({
  stega: {studioUrl: 'https://your-project-name.sanity.studio', logger: console},
})

Removing stega from part of the result, available on [@sanity/client/stega]:

import {stegaClean} from '@sanity/client/stega'
const result = await client.fetch('*[_type == "video"][0]')

// Remove stega from the payload sent to a third party library
const videoAsset = stegaClean(result.videoAsset)

Creating Studio edit intent links

If you want to create an edit link to something that isn't a string, or a field that isn't rendered directly, like a slug used in a URL but not rendered on the page, you can use the resolveEditUrl function.

import {createClient} from '@sanity/client'
import {resolveEditUrl} from '@sanity/client/csm'

const client = createClient({
  // ... standard client config
  // Required: the new 'withKeyArraySelector' option is used here instead of 'true' so that links to array items and portable text are stable even if the array is reordered
  resultSourceMap: 'withKeyArraySelector',
})
const {result, resultSourceMap} = await client.fetch(
  `*[_type == "author" && slug.current == $slug][0]{name, pictures}`,
  {slug: 'john-doe'},
  // Required, otherwise you can't access `resultSourceMap`
  {filterResponse: false},
)

// The `result` looks like this:
const result = {
  name: 'John Doe',
  pictures: [
    {
      _type: 'image',
      alt: 'A picture of exactly what you think someone named John Doe would look like',
      _key: 'cee5fbb69da2',
      asset: {
        _ref: 'image-a75b03fdd5b5fa36947bf2b776a542e0c940f682-1000x1500-jpg',
        _type: 'reference',
      },
    },
  ],
}

const studioUrl = 'https://your-project-name.sanity.studio'

resolveEditUrl({
  // The URL resolver requires the base URL of your Sanity Studio instance
  studioUrl,
  // It also requires a Content Source Map for the query result you want to create an edit intent link for
  resultSourceMap,
  // The path to the field you want to edit. You can pass a string
  resultPath: 'pictures[0].alt',
  // or an array of segments
  resultPath: ['pictures', 0, 'alt'],
})
// ^? 'https://your-project-name.sanity.studio/intent/edit/mode=presentation;id=462efcc6-3c8b-47c6-8474-5544e1a4acde;type=author;path=pictures[_key=="cee5fbb69da2"].alt'

Listening to queries

const query = '*[_type == "comment" && authorId != $ownerId]'
const params = {ownerId: 'bikeOwnerUserId'}

const subscription = client.listen(query, params).subscribe((update) => {
  const comment = update.result
  console.log(`${comment.author} commented: ${comment.text}`)
})

// to unsubscribe later on
subscription.unsubscribe()

client.listen(query, params = {}, options = {includeResult: true})

Open a query that listens for updates on matched documents, using the given parameters (if any). The return value is an RxJS Observable. When calling .subscribe() on the returned observable, a subscription is returned, and this can be used to unsubscribe from the query later on by calling subscription.unsubscribe()

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.

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.

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.

If it's not relevant to know what mutations that was applied, you can also set includeMutation to false in the options, which will save some additional bandwidth by omitting the mutation property from the received events.

Fetch a single document

This will fetch a document from the Doc endpoint. 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.

client.getDocument('bike-123').then((bike) => {
  console.log(`${bike.name} (${bike.seats} seats)`)
})

Fetch multiple documents in one go

This will fetch multiple documents in one request from the Doc endpoint. 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.

client.getDocuments(['bike123', 'bike345']).then(([bike123, bike345]) => {
  console.log(`Bike 123: ${bike123.name} (${bike123.seats} seats)`)
  console.log(`Bike 345: ${bike345.name} (${bike345.seats} seats)`)
})

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:

const ids = ['bike123', 'nonexistent-document', 'bike345']
client.getDocuments(ids).then((docs) => {
  // the docs array will be:
  // [{_id: 'bike123', ...}, null, {_id: 'bike345', ...}]
})

Creating documents

const doc = {
  _type: 'bike',
  name: 'Sanity Tandem Extraordinaire',
  seats: 2,
}

client.create(doc).then((res) => {
  console.log(`Bike was created, document ID is ${res._id}`)
})

client.create(doc) client.create(doc, mutationOptions)

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.

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 .).

Creating/replacing documents

const doc = {
  _id: 'my-bike',
  _type: 'bike',
  name: 'Sanity Tandem Extraordinaire',
  seats: 2,
}

client.createOrReplace(doc).then((res) => {
  console.log(`Bike was created, document ID is ${res._id}`)
})

client.createOrReplace(doc) client.createOrReplace(doc, mutationOptions)

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.

Creating if not already present

const doc = {
  _id: 'my-bike',
  _type: 'bike',
  name: 'Sanity Tandem Extraordinaire',
  seats: 2,
}

client.createIfNotExists(doc).then((res) => {
  console.log('Bike was created (or was already present)')
})

client.createIfNotExists(doc) client.createIfNotExists(doc, mutationOptions)

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.

Patch/update a document

client
  .patch('bike-123') // Document ID to patch
  .set({inStock: false}) // Shallow merge
  .inc({numSold: 1}) // Increment field by count
  .commit() // Perform the patch and return a promise
  .then((updatedBike) => {
    console.log('Hurray, the bike is updated! New document:')
    console.log(updatedBike)
  })
  .catch((err) => {
    console.error('Oh no, the update failed: ', err.message)
  })

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.

client.patch()
  [operations]
  .commit(mutationOptions)

Setting a field only if not already present

client.patch('bike-123').setIfMissing({title: 'Untitled bike'}).commit()

Removing/unsetting fields

client.patch('bike-123').unset(['title', 'price']).commit()

Incrementing/decrementing numbers

client
  .patch('bike-123')
  .inc({price: 88, numSales: 1}) // Increment `price` by 88, `numSales` by 1
  .dec({inStock: 1}) // Decrement `inStock` by 1
  .commit()

Patch a document only if revision matches

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.

client
  .patch('bike-123')
  .ifRevisionId('previously-known-revision')
  .set({title: 'Little Red Tricycle'})
  .commit()

Adding elements to an array

The patch operation insert takes a location (before, after or replace), a path selector and an array of elements to insert.

client
  .patch('bike-123')
  // Ensure that the `reviews` arrays exists before attempting to add items to it
  .setIfMissing({reviews: []})
  // Add the items after the last item in the array (append)
  .insert('after', 'reviews[-1]', [{title: 'Great bike!', stars: 5}])
  .commit({
    // Adds a `_key` attribute to array items, unique within the array, to
    // ensure it can be addressed uniquely in a real-time collaboration context
    autoGenerateArrayKeys: true,
  })

Appending/prepending elements to an array

The operations of appending and prepending to an array are so common that they have been given their own methods for better readability:

client
  .patch('bike-123')
  .setIfMissing({reviews: []})
  .append('reviews', [{title: 'Great bike!', stars: 5}])
  .commit({autoGenerateArrayKeys: true})

Deleting an element from an array

Each entry in the unset array can be either an attribute or a JSON path.

In this example, we remove the first review and the review with _key: 'abc123' from the bike.reviews array:

const reviewsToRemove = ['reviews[0]', 'reviews[_key=="abc123"]']
client.patch('bike-123').unset(reviewsToRemove).commit()

Delete documents

A single document can be deleted by specifying a document ID:

client.delete(docId) client.delete(docId, mutationOptions)

client
  .delete('bike-123')
  .then(() => {
    console.log('Bike deleted')
  })
  .catch((err) => {
    console.error('Delete failed: ', err.message)
  })

One or more documents can be deleted by specifying a GROQ query (and optionally, params):

client.delete({ query: "GROQ query", params: { key: value } })

// Without params
client
  .delete({query: '*[_type == "bike"][0]'})
  .then(() => {
    console.log('The document matching *[_type == "bike"][0] was deleted')
  })
  .catch((err) => {
    console.error('Delete failed: ', err.message)
  })
// With params
client
  .delete({query: '*[_type == $type][0..1]', params: {type: 'bike'}})
  .then(() => {
    console.log('The documents matching *[_type == "bike"][0..1] was deleted')
  })
  .catch((err) => {
    console.error('Delete failed: ', err.message)
  })

Multiple mutations in a transaction

const namePatch = client.patch('bike-310').set({name: 'A Bike To Go'})

client
  .transaction()
  .create({name: 'Sanity Tandem Extraordinaire', seats: 2})
  .delete('bike-123')
  .patch(namePatch)
  .commit()
  .then((res) => {
    console.log('Whole lot of stuff just happened')
  })
  .catch((err) => {
    console.error('Transaction failed: ', err.message)
  })

client.transaction().create(doc).delete(docId).patch(patch).commit()

Create a transaction to perform chained mutations.

client
  .transaction()
  .create({name: 'Sanity Tandem Extraordinaire', seats: 2})
  .patch('bike-123', (p) => p.set({inStock: false}))
  .commit()
  .then((res) => {
    console.log('Bike created and a different bike is updated')
  })
  .catch((err) => {
    console.error('Transaction failed: ', err.message)
  })

client.transaction().create(doc).patch(docId, p => p.set(partialDoc)).commit()

A patch can be performed inline on a transaction.

Clientless patches & transactions

Transactions and patches can also be built outside the scope of a client:

import {createClient, Patch, Transaction} from '@sanity/client'
const client = createClient({
  projectId: 'your-project-id',
  dataset: 'bikeshop',
})

// Patches:
const patch = new Patch('<documentId>')
client.mutate(patch.inc({count: 1}).unset(['visits']))

// Transactions:
const transaction = new Transaction().create({_id: '123', name: 'FooBike'}).delete('someDocId')

client.mutate(transaction)

const patch = new Patch(docId)

const transaction = new Transaction()

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()

Actions

The Actions API provides a new interface for creating, updating and publishing documents. It is a wrapper around the Actions API.

This API is only available from API version v2024-05-23.

Action options

The following options are available for actions, and can be applied as the second argument to action().

  • transactionId: If set, this ID is as transaction ID for the action instead of using an autogenerated one.
  • 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.
  • skipCrossDatasetReferenceValidation (true|false) - default false. If true, the mutation will be skipped validation of cross dataset references. This is useful when you are creating a document that references a document in a different dataset, and you want to skip the validation to avoid an error.

Create Action

A document draft can be created by specifying a create action type:

client
  .action(
    {
      actionType: 'sanity.action.document.create',
      publishedId: 'bike-123',
      attributes: {name: 'Sanity Tandem Extraordinaire', _type: 'bike', seats: 1},
      ifExists: 'fail',
    },
    actionOptions,
  )
  .then(() => {
    console.log('Bike draft created')
  })
  .catch((err) => {
    console.error('Create draft failed: ', err.message)
  })

Delete Action

A published document can be deleted by specifying a delete action type, optionally including some drafts:

client
  .action(
    {
      actionType: 'sanity.action.document.delete',
      publishedId: 'bike-123',
      includeDrafts: ['draft.bike-123'],
    },
    actionOptions,
  )
  .then(() => {
    console.log('Bike deleted')
  })
  .catch((err) => {
    console.error('Delete failed: ', err.message)
  })

Discard Action

A draft document can be deleted by specifying a discard action type:

client
  .action(
    {
      actionType: 'sanity.action.document.discard',
      draftId: 'draft.bike-123',
    },
    actionOptions,
  )
  .then(() => {
    console.log('Bike draft deleted')
  })
  .catch((err) => {
    console.error('Discard failed: ', err.message)
  })

Edit Action

A patch can be applied to an existing document draft or create a new one by specifying an edit action type:

client
  .action(
    {
      actionType: 'sanity.action.document.edit',
      publishedId: 'bike-123',
      attributes: {name: 'Sanity Tandem Extraordinaire', _type: 'bike', seats: 2},
    },
    actionOptions,
  )
  .then(() => {
    console.log('Bike draft edited')
  })
  .catch((err) => {
    console.error('Edit draft failed: ', err.message)
  })

Publish Action

A draft document can be published by specifying a publish action type, optionally with revision ID checks:

client
  .action(
    {
      actionType: 'sanity.action.document.publish',
      draftId: 'draft.bike-123',
      ifDraftRevisionId: '<previously-known-revision>',
      publishedId: 'bike-123',
      ifPublishedRevisionId: '<previously-known-revision>',
    },
    actionOptions,
  )
  .then(() => {
    console.log('Bike draft published')
  })
  .catch((err) => {
    console.error('Publish draft failed: ', err.message)
  })

ReplaceDraft Action

An existing document draft can be deleted and replaced by a new one by specifying a replaceDraft action type:

client
  .action(
    {
      actionType: 'sanity.action.document.replaceDraft',
      publishedId: 'bike-123',
      attributes: {name: 'Sanity Tandem Extraordinaire', _type: 'bike', seats: 1},
    },
    actionOptions,
  )
  .then(() => {
    console.log('Bike draft replaced')
  })
  .catch((err) => {
    console.error('Replace draft failed: ', err.message)
  })

Unpublish Action

A published document can be retracted by specifying an unpublish action type:

client
  .action(
    {
      actionType: 'sanity.action.document.unpublish',
      draftId: 'draft.bike-123',
      publishedId: 'bike-123',
    },
    actionOptions,
  )
  .then(() => {
    console.log('Bike draft unpublished')
  })
  .catch((err) => {
    console.error('Unpublish draft failed: ', err.message)
  })

Uploading assets

Assets can be uploaded using the client.assets.upload(...) method.

client.assets.upload(type: 'file' | image', body: File | Blob | Buffer | NodeJS.ReadableStream, options = {}): Promise<AssetDocument>

👉 Read more about assets in Sanity

Examples: Uploading assets from Node.js

// Upload a file from the file system
client.assets
  .upload('file', fs.createReadStream('myFile.txt'), {filename: 'myFile.txt'})
  .then((document) => {
    console.log('The file was uploaded!', document)
  })
  .catch((error) => {
    console.error('Upload failed:', error.message)
  })
// Upload an image file from the file system
client.assets
  .upload('image', fs.createReadStream('myImage.jpg'), {filename: 'myImage.jpg'})
  .then((document) => {
    console.log('The image was uploaded!', document)
  })
  .catch((error) => {
    console.error('Upload failed:', error.message)
  })

Examples: Uploading assets from the Browser

// Create a file with "foo" as its content
const file = new File(['foo'], 'foo.txt', {type: 'text/plain'})
// Upload it
client.assets
  .upload('file', file)
  .then((document) => {
    console.log('The file was uploaded!', document)
  })
  .catch((error) => {
    console.error('Upload failed:', error.message)
  })
// Draw something on a canvas and upload as image
const canvas = document.getElementById('someCanvas')
const ctx = canvas.getContext('2d')
ctx.fillStyle = '#f85040'
ctx.fillRect(0, 0, 50, 50)
ctx.fillStyle = '#fff'
ctx.font = '10px monospace'
ctx.fillText('Sanity', 8, 30)
canvas.toBlob(uploadImageBlob, 'image/png')

function uploadImageBlob(blob) {
  client.assets
    .upload('image', blob, {contentType: 'image/png', filename: 'someText.png'})
    .then((document) => {
      console.log('The image was uploaded!', document)
    })
    .catch((error) => {
      console.error('Upload failed:', error.message)
    })
}

Examples: Specify image metadata to extract

// Extract palette of colors as well as GPS location from exif
client.assets
  .upload('image', someFile, {extract: ['palette', 'location']})
  .then((document) => {
    console.log('The file was uploaded!', document)
  })
  .catch((error) => {
    console.error('Upload failed:', error.message)
  })

Deleting an asset

Deleting an asset document will also trigger deletion of the actual asset.

client.delete(assetDocumentId: string): Promise
client.delete('image-abc123_someAssetId-500x500-png').then((result) => {
  console.log('deleted imageAsset', result)
})

Mutation options

The following options are available for mutations, and can be applied either as the second argument to create(), createOrReplace, createIfNotExists, delete() and mutate() - or as an argument to the commit() method on patches and transactions.

  • visibility ('sync'|'async'|'deferred') - default 'sync'
    • sync: request will not return until the requested changes are visible to subsequent queries.
    • 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.
    • 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.
  • 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.
  • autoGenerateArrayKeys (true|false) - default false. If true, the mutation API will automatically add _key attributes to objects in arrays that are 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.

Aborting a request

Requests can be aborted (or cancelled) in two ways:

1. Abort a request by passing an AbortSignal with the request options

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:

const abortController = new AbortController()

// note the lack of await here
const request = getClient().fetch('*[_type == "movie"]', {}, {signal: abortController.signal})

// this will abort the request after 200ms
setTimeout(() => abortController.abort(), 200)

try {
  const response = await request
  //…
} catch (error) {
  if (error.name === 'AbortError') {
    console.log('Request was aborted')
  } else {
    // rethrow in case of other errors
    throw error
  }
}

2. Cancel a request by unsubscribing from the Observable

When using the Observable API (e.g. client.observable.fetch()), you can cancel the request by simply unsubscribe from the returned observable:

const subscription = client.observable.fetch('*[_type == "movie"]').subscribe((result) => {
  /* do something with the result */
})

// this will cancel the request
subscription.unsubscribe()

Get client configuration

const config = client.config()
console.log(config.dataset)

client.config()

Get client configuration.

Set client configuration

client.config({dataset: 'newDataset'})

client.config(options)

Set client configuration. Required options are projectId and dataset.

License

MIT © Sanity.io

Migrate

From v5

The default useCdn is changed to true

It was previously false. If you were relying on the default being false you can continue using the live API by setting it in the constructor:

import {createClient} from '@sanity/client'

export const client = createClient({
  projectId: 'your-project-id',
  dataset: 'your-dataset-name',
  apiVersion: '2023-03-12',
+ useCdn: false, // set to `true` to use the edge cache
})

From v4

No longer shipping ES5

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.

Node.js v12 no longer supported

Upgrade to the LTS release, or one of the Maintenance releases.

The default export is replaced with the named export createClient

Before:

import createClient from '@sanity/client'
const client = createClient()
import SanityClient from '@sanity/client'
const client = new SanityClient()

After:

import {createClient} from '@sanity/client'
const client = createClient()

client.assets.delete is removed

Before:

client.assets.delete('image', 'abc123_foobar-123x123-png')
client.assets.delete('image', 'image-abc123_foobar-123x123-png')
client.assets.delete({_id: 'image-abc123_foobar-123x123-png'})

After:

client.delete('image-abc123_foobar-123x123-png')

client.assets.getImageUrl is removed, replace with @sanity/image-url

Before:

import createClient from '@sanity/client'
const client = createClient({projectId: 'abc123', dataset: 'foo'})

client.assets.getImageUrl('image-abc123_foobar-123x123-png')
client.assets.getImageUrl('image-abc123_foobar-123x123-png', {auto: 'format'})
client.assets.getImageUrl({_ref: 'image-abc123_foobar-123x123-png'})
client.assets.getImageUrl({_ref: 'image-abc123_foobar-123x123-png'}, {auto: 'format'})

After:

npm install @sanity/image-url
import imageUrlBuilder from '@sanity/image-url'
const builder = imageUrlBuilder({projectId: 'abc123', dataset: 'foo'})
const urlFor = (source) => builder.image(source)

urlFor('image-abc123_foobar-123x123-png').url()
urlFor('image-abc123_foobar-123x123-png').auto('format').url()
urlFor({_ref: 'image-abc123_foobar-123x123-png'}).url()
urlFor({_ref: 'image-abc123_foobar-123x123-png'}).auto('format').url()

SanityClient static properties moved to named exports

Before:

import SanityClient from '@sanity/client'

const {Patch, Transaction, ClientError, ServerError, requester} = SanityClient

After:

import {Patch, Transaction, ClientError, ServerError, requester} from '@sanity/client'

client.clientConfig is removed, replace with client.config()

Before:

import createClient from '@sanity/client'
const client = createClient()

console.log(client.clientConfig.projectId)

After:

import {createClient} from '@sanity/client'
const client = createClient()

console.log(client.config().projectId)

client.isPromiseAPI() is removed, replace with an instanceof check

Before:

import createClient from '@sanity/client'
const client = createClient()

console.log(client.isPromiseAPI())
console.log(client.clientConfig.isPromiseAPI)
console.log(client.config().isPromiseAPI)

After:

import {createClient, SanityClient} from '@sanity/client'
const client = createClient()

console.log(client instanceof SanityClient)

client.observable.isObservableAPI() is removed, replace with an instanceof check

Before:

import createClient from '@sanity/client'
const client = createClient()

console.log(client.observable.isObservableAPI())

After:

import {createClient, ObservableSanityClient} from '@sanity/client'
const client = createClient()

console.log(client.observable instanceof ObservableSanityClient)

client._requestObservable is removed, replace with client.observable.request

Before:

import createClient from '@sanity/client'
const client = createClient()

client._requestObservable({uri: '/ping'}).subscribe()

After:

import {createClient} from '@sanity/client'
const client = createClient()

client.observable.request({uri: '/ping'}).subscribe()

client._dataRequest is removed, replace with client.dataRequest

Before:

import createClient from '@sanity/client'
const client = createClient()

client._dataRequest(endpoint, body, options)

After:

import {createClient} from '@sanity/client'
const client = createClient()

client.dataRequest(endpoint, body, options)

client._create_ is removed, replace with one of client.create, client.createIfNotExists or client.createOrReplace

Before:

import createClient from '@sanity/client'
const client = createClient()

client._create(doc, 'create', options)
client._create(doc, 'createIfNotExists', options)
client._create(doc, 'createOrReplace', options)

After:

import {createClient} from '@sanity/client'
const client = createClient()

client.create(doc, options)
client.createIfNotExists(doc, options)
client.createOrReplace(doc, options)

client.patch.replace is removed, replace with client.createOrReplace

Before:

import createClient from '@sanity/client'
const client = createClient()

client.patch('tropic-hab').replace({name: 'Tropical Habanero', ingredients: []}).commit()

After:

import {createClient} from '@sanity/client'
const client = createClient()

client.createOrReplace({
  _id: 'tropic-hab',
  _type: 'hotsauce',
  name: 'Tropical Habanero',
  ingredients: [],
})

client.auth is removed, replace with client.request

Before:

import createClient from '@sanity/client'
const client = createClient()

/**
 * Fetch available login providers
 */
const loginProviders = await client.auth.getLoginProviders()
/**
 * Revoke the configured session/token
 */
await client.auth.logout()

After:

import {createclient, type AuthProviderResponse} from '@sanity/client'
const client = createClient()

/**
 * Fetch available login providers
 */
const loginProviders = await client.request<AuthProviderResponse>({uri: '/auth/providers'})
/**
 * Revoke the configured session/token
 */
await client.request<void>({uri: '/auth/logout', method: 'POST'})

client's People

Contributors

bjoerge avatar ecospark[bot] avatar geball avatar github-actions[bot] avatar j33ty avatar judofyr avatar juice49 avatar kmelve avatar kristofferjs avatar mariuslundgard avatar rdunk avatar renovate[bot] avatar rexxars avatar ricokahler avatar ritadias avatar runeb avatar runeh avatar saasen avatar semantic-release-bot avatar sgulseth avatar simeongriggs avatar simonxcode avatar skogsmaskin avatar stipsan avatar stoivo avatar stuymedova avatar svirs avatar tomsseisums avatar tzhelyazkova avatar vicmeow avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

client's Issues

Inconsistent data between `/data/doc` and `data/query`

Normally this document shouldn't exist as it was deleted using cleint.delete but for some reason, it still exists on /query as a full document, and on the studio as an empty document!

Screenshot 2023-08-06 at 16 53 06

Making an HTTP request to the /query endpoint

https://<<acc_id>>.api.sanity.io/v2021-06-07/data/query/production/?query=*[_id == "1GTUUGED6PZ227984"]{_id}

Returns (normally it should not return the document as it was deleted!)

{
    "query": "*[_id == \"1GTUUGED6PZ227984\"]{_id}",
    "result": [
        {
            "_id": "1GTUUGED6PZ227984"
        }
    ],
    "ms": 13
}

Making same HTTP request to /doc endpoint

https:/<<acc_id>>.api.sanity.io/v2021-06-07/data/doc/production/1GTUUGED6PZ227984

Returns (which is correct)

{
    "documents": [],
    "omitted": [
        {
            "id": "1GTUUGED6PZ227984",
            "reason": "existence"
        }
    ]
}

P.S. It's not only one single document, it's more than 200 documents.

Proposal for runtime safe typed GROQ

TypeScript Playground example implementation

Situation

It's useful to type GROQ responses (client.fetch) in a way that's runtime safe.
It brings a lot of value as users can make sure the way they're using the data they client.fetch are safe from runtime bugs and there's no risk of unexpected errors.
Providing typings on the responses also gives auto-complete support for them so people can write their app code much faster and with less risk of typos.

Complication

What we offer today is just a generic that lets you specify the response:

const data = await client.fetch<[title: string]{}>(`*[]{title}`)
// data is typed to `{title: string}[]`

But client isn't runtime checking this so it's possible that the dataset contains documents that don't define title, or maybe sets it to null.
Which could lead to runtime errors that TypeScript isn't catching since it's trusting the typings provided to the generic to be "truth":

data.map(({title}) => data.title.toUpperCase()) // this can throw at runtime if `title` is `null` or `undefined`

Question

How can we offer runtime safety, or at least make it easier for other libraries to provide it?
Can we offer it in a way that doesn't increase the bundle size for people that won't use this feature?

Answer

Libraries like groqd have started using the convention of taking a GROQ string as input, and output an object like: {query: string, schema: {parse(data: unknown): TypesafeResponse}}.

We can support this convention, making it very ergonomic for libraries like groqd to provide the heavy lifting while the Sanity Client stays lightweight.

Suggested API:

import {createClient} from '@sanity/client'

const client = createClient()

const custom = await client.fetch({
  query: `count(*)`,
  schema: {
    parse(data: unknown): number {
      if (isNumber(data)) {
        return data
      }
      throw new TypeError(`data isn't a number`)
    },
  },
})

function isNumber(data: unknown): data is number {
  return Number.isFinite(data)
}

The reason why it makes sense to use {query: string, schema: {parse(data: unknown): TypedResponse}} as opposed to a simpler {query: string, schema(data: unknown): TypedResponse} or {query: string, parse(data: unknown): TypedResponse} is it makes it easier for libraries to support letting userland customise the parser. They might want narrower types than their library provides out of the box, maybe widen them or perhaps coerce date strings to Date instances.

There's more examples exploring this in the playground. As well as the companion codesandbox.

Cannot find name 'File' error after v5 upgrade (in Node)

Hi Sanity team

A similar issue to this one happens after the 5.2.1 update (console below)

In Node if the tsconfig / lib options doesn't include 'dom' option (which can happen if you are using this package on server side)

node_modules/.pnpm/@[email protected]/node_modules/@sanity/client/dist/index.d.ts:47:11 - error TS2304: Cannot find name 'File'.

47     body: File | Blob | Buffer | NodeJS.ReadableStream,
             ~~~~

node_modules/.pnpm/@[email protected]/node_modules/@sanity/client/dist/index.d.ts:59:11 - error TS2304: Cannot find name 'File'.

59     body: File | Blob | Buffer | NodeJS.ReadableStream,
             ~~~~

node_modules/.pnpm/@[email protected]/node_modules/@sanity/client/dist/index.d.ts:71:11 - error TS2304: Cannot find name 'File'.

71     body: File | Blob | Buffer | NodeJS.ReadableStream,
             ~~~~

node_modules/.pnpm/@[email protected]/node_modules/@sanity/client/dist/index.d.ts:589:11 - error TS2304: Cannot find name 'File'.

589     body: File | Blob | Buffer | NodeJS.ReadableStream,
              ~~~~

node_modules/.pnpm/@[email protected]/node_modules/@sanity/client/dist/index.d.ts:605:11 - error TS2304: Cannot find name 'File'.

605     body: File | Blob | Buffer | NodeJS.ReadableStream,
              ~~~~

node_modules/.pnpm/@[email protected]/node_modules/@sanity/client/dist/index.d.ts:621:11 - error TS2304: Cannot find name 'File'.

621     body: File | Blob | Buffer | NodeJS.ReadableStream,

`perspective` not always included in query

I'm experiencing an issue where sometimes including perspective: published in my client config still returns the results of a raw query.

Digging into it a bit it seems like if the groq query is too large the perspective param isn't included. "Too large" seems to be past the threshold where the query is included in the body of the request instead of as a query param.

Using v6.1.5

Adding Sanity client breaks new Hydrogen storefront

Steps to reproduce

  1. Create a new Hydrogen app: https://shopify.dev/docs/custom-storefronts/hydrogen/getting-started/quickstart
  2. Choose hello world template
  3. Choose TypeScript
  4. Install dependencies
  5. Install Sanity client
  6. Run npm run dev and see it work
  7. Create new Sanity client in the loader function in root.tsx
  8. Run npm run dev and see it break

Expected behavior

Adding Sanity client does not break anything, and works.

Actual behavior

Adding Sanity client throws TypeError: globalThis.XMLHttpRequest is not a constructor and does not work.

Sanity client: updating a field on an object

Is your feature request related to a problem? Please describe.
I'm using sanity client to update one field on an object. The only way to update the object is via client.patch('id').set(fieldName: value}), but when value is an object, it overwrites the entire object on the document. Only the field specified would be nice.

Describe the solution you'd like
If an object field is set in the client, only update the fields specified in the object. Don't overwrite the entire object.

Additional context
I'd get by if I had access to the publish action's draft, so I could use the spread operator to create the correct object. Something like:

  1. get the most recent draft of the document
  2. Use the client to patch the document like: client.patch('id').set(article: { ...article, publishDate: <new date> })

Unable to upload image asset (as Blob) from NextJS Route Handler

When trying to upload an image as Blob using the client.assets.upload method inside the Nextjs 13 Route Handler, the method throws error:

Error: Request body must be a string, buffer or stream, got object
    at httpRequester (webpack-internal:///(sc_server)/./node_modules/get-it/dist/index.cjs:468:15)
    at Object.eval (webpack-internal:///(sc_server)/./node_modules/get-it/dist/index.cjs:134:30)
    at Object.publish (webpack-internal:///(sc_server)/./node_modules/get-it/dist/index.cjs:80:28)
    at Observable.eval [as _subscribe] (webpack-internal:///(sc_server)/./node_modules/get-it/dist/middleware.cjs:311:34)
    at Observable._trySubscribe (webpack-internal:///(sc_server)/./node_modules/rxjs/dist/cjs/internal/Observable.js:36:25)
    at eval (webpack-internal:///(sc_server)/./node_modules/rxjs/dist/cjs/internal/Observable.js:30:121)
    at Object.errorContext (webpack-internal:///(sc_server)/./node_modules/rxjs/dist/cjs/internal/util/errorContext.js:26:9)
    at Observable.subscribe (webpack-internal:///(sc_server)/./node_modules/rxjs/dist/cjs/internal/Observable.js:28:24)
    at Observable.eval [as _subscribe] (webpack-internal:///(sc_server)/./node_modules/@sanity/client/dist/index.cjs:815:51)
    at Observable._trySubscribe (webpack-internal:///(sc_server)/./node_modules/rxjs/dist/cjs/internal/Observable.js:36:25)
    at eval (webpack-internal:///(sc_server)/./node_modules/rxjs/dist/cjs/internal/Observable.js:30:121)
    at Object.errorContext (webpack-internal:///(sc_server)/./node_modules/rxjs/dist/cjs/internal/util/errorContext.js:26:9)
    at Observable.subscribe (webpack-internal:///(sc_server)/./node_modules/rxjs/dist/cjs/internal/Observable.js:28:24)
    at eval (webpack-internal:///(sc_server)/./node_modules/rxjs/dist/cjs/internal/operators/filter.js:11:16)
    at OperatorSubscriber.eval (webpack-internal:///(sc_server)/./node_modules/rxjs/dist/cjs/internal/util/lift.js:16:28)
    at eval (webpack-internal:///(sc_server)/./node_modules/rxjs/dist/cjs/internal/Observable.js:30:48)
    at Object.errorContext (webpack-internal:///(sc_server)/./node_modules/rxjs/dist/cjs/internal/util/errorContext.js:26:9)
    at Observable.subscribe (webpack-internal:///(sc_server)/./node_modules/rxjs/dist/cjs/internal/Observable.js:28:24)
    at eval (webpack-internal:///(sc_server)/./node_modules/rxjs/dist/cjs/internal/operators/map.js:11:16)
    at SafeSubscriber.eval (webpack-internal:///(sc_server)/./node_modules/rxjs/dist/cjs/internal/util/lift.js:16:28)
    at eval (webpack-internal:///(sc_server)/./node_modules/rxjs/dist/cjs/internal/Observable.js:30:48)
    at Object.errorContext (webpack-internal:///(sc_server)/./node_modules/rxjs/dist/cjs/internal/util/errorContext.js:26:9)
    at Observable.subscribe (webpack-internal:///(sc_server)/./node_modules/rxjs/dist/cjs/internal/Observable.js:28:24)
    at eval (webpack-internal:///(sc_server)/./node_modules/rxjs/dist/cjs/internal/lastValueFrom.js:12:16)
    at new Promise (<anonymous>)
    at Object.lastValueFrom (webpack-internal:///(sc_server)/./node_modules/rxjs/dist/cjs/internal/lastValueFrom.js:9:12)
    at AssetsClient.upload (webpack-internal:///(sc_server)/./node_modules/@sanity/client/dist/index.cjs:901:21)

The code is as following:

import { createClient } from '@sanity/client';
const client = createClient({
  projectId: 'sdfsd',
  dataset: 'sdfdsf,
  apiVersion: '2023-02-23',
  useCdn: false,
  token: process.env.SANITY_TOKEN,
});

The route handler file /app/api/myform/route.ts

import { type NextRequest, NextResponse } from 'next/server';
export const dynamic = 'force-dynamic';

export async function POST(req: NextRequest) {
   const form = await req.formData();
   const file = form.get('file') as Blob;

   if (!file) {
      return new NextResponse('Bad request', { status: 400 });
   }

   await client.assets.upload('image', file, { filename: 'image'});
   return NextResponse.json({});
}

I checked that the blob is valid and has the proper image file.
I guess the get-it package under the hood always expects Nodejs type of body (not the browser/Web API)

There is a duplicate issue submitted for the same problem on next-sanity repo sanity-io/next-sanity#348

client.users.getById() type is too strict

user.getById() types feels too strict to me.

Right not it looks like:

getById<T extends 'me' | string>(
  id: T
): T extends 'me' ? Observable<CurrentSanityUser> : Observable<SanityUser>

I am actually using this method by passing a string with several IDs separated with a comma, in this case the result looks like it should be typed with Observable<SanityUser[]> (it's an array).

Also, the current type of SanityUser includes a required isCurrentUser boolean property which is missing from my requests.

Property '#private' in type 'SanityClient'

Argument of type 'import("/project/node_modules/@sanity/types/node_modules/@sanity/client/dist/index").SanityClient' is not assignable to parameter of type 'import("/project/node_modules/@sanity/client/dist/index").SanityClient'.
  Property '#private' in type 'SanityClient' refers to a different member that cannot be accessed from within type 'SanityClient'

Occurs when using useClient({ apiVersion: '2022-03-13' }) for a value against import type { SanityClient } from '@sanity/client';

Expected

SanityClient type should be accessible

Improve confusing error messages for JS client

Is your feature request related to a problem? Please describe.
Syntax errors within GROQ queries throw a 'Must be an attribute or string key' error. The stacktrace doesn't give a great deal of information as to where the problem is. Example below:

Very simple mistake to make. Missing comma after 'slug':

`
    {
      "work": *[_type == "caseStudies"]{
        title,
        slug
        tags,
      }
    }
  `

Error message given:

Must be an attribute or a string keyClientError: Must be an attribute or a string key
    at onResponse (/Users/craig/Documents/projects/flo-site-v5/node_modules/@sanity/client/lib/http/request.js:27:13)
    at /Users/craig/Documents/projects/flo-site-v5/node_modules/get-it/lib-node/util/middlewareReducer.js:10:22
    at Array.reduce (<anonymous>)
    at applyMiddleware (/Users/craig/Documents/projects/flo-site-v5/node_modules/get-it/lib-node/util/middlewareReducer.js:9:29)
    at onResponse (/Users/craig/Documents/projects/flo-site-v5/node_modules/get-it/lib-node/index.js:81:22)
    at /Users/craig/Documents/projects/flo-site-v5/node_modules/get-it/lib-node/index.js:48:55
    at callback (/Users/craig/Documents/projects/flo-site-v5/node_modules/get-it/lib-node/request/node-request.js:57:46)
    at /Users/craig/Documents/projects/flo-site-v5/node_modules/get-it/lib-node/request/node-request.js:141:14
    at DestroyableTransform.<anonymous> (/Users/craig/Documents/projects/flo-site-v5/node_modules/simple-concat/index.js:7:13)
    at Object.onceWrapper (events.js:312:28)
    at DestroyableTransform.emit (events.js:228:7)
    at endReadableNT (/Users/craig/Documents/projects/flo-site-v5/node_modules/readable-stream/lib/_stream_readable.js:1010:12)
    at processTicksAndRejections (internal/process/task_queues.js:81:21)

Describe the solution you'd like
Relevant error messages, perhaps as a result of validation of the GROQ query itself.

Build rate-limiting into the client

I spent a fair bit of time today redoing some of my project code to allow for the rate limiting that got turned on last night. I have no issue with rate limiting but it seems to me that it would be much more convenient, given it's such a cross-cutting concern, and can be non-trivial to implement, to just have the rate limiting built into the javascript client itself. Then it's just not something that we devs need to worry our pretty little heads over.

Client hangs if blockContent contains empty span

Hi there, this JS client seems to hang if there are "text": "" spans in the response from Sanity API. The HTTP endpoint returns the below JSON fine, but the JS client with the same query will hang indefinitely (even if I set a timeout).

It seems like something in the client's processing of responses is hanging forever in some circumstances.

If I change the content in the manager, then the client will start working again.

It often seems worse if it's blank lines under an H1 tag? But I've seen it happen in a few different circumstances. Currently it makes the client pretty much unuseable for me.

Response from HTTP API:

{
    "ms": 6,
    "query": "*[_type == \"post\"]",
    "result": [
        {
            "_createdAt": "2022-11-09T20:13:54Z",
            "_id": "e3d30547-8ee0-42d5-b409-3cf52a208ebc",
            "_rev": "UckHRufVFepji8vDPE9wNu",
            "_type": "post",
            "_updatedAt": "2022-11-09T22:12:57Z",
            "author": {
                "_ref": "ef72f677-3ad6-4975-b4e9-3c585731fb83",
                "_type": "reference"
            },
            "body": [
                {
                    "_key": "aff5062c1d62",
                    "_type": "block",
                    "children": [
                        {
                            "_key": "290029bb1ffc",
                            "_type": "span",
                            "marks": [],
                            "text": "Normal text"
                        }
                    ],
                    "markDefs": [],
                    "style": "normal"
                },
                {
                    "_key": "8d3b960961df",
                    "_type": "block",
                    "children": [
                        {
                            "_key": "3ad547385583",
                            "_type": "span",
                            "marks": [],
                            "text": "H1 text"
                        }
                    ],
                    "markDefs": [],
                    "style": "h1"
                },
                {
                    "_key": "faac7482a0a3",
                    "_type": "block",
                    "children": [
                        {
                            "_key": "dc73e2904a1e",
                            "_type": "span",
                            "marks": [],
                            "text": "Normal text"
                        }
                    ],
                    "markDefs": [],
                    "style": "normal"
                },
                {
                    "_key": "269046411e24",
                    "_type": "block",
                    "children": [
                        {
                            "_key": "8d18d6bdc5d6",
                            "_type": "span",
                            "marks": [],
                            "text": ""
                        }
                    ],
                    "markDefs": [],
                    "style": "normal"
                },
                {
                    "_key": "7dc460534a6a",
                    "_type": "block",
                    "children": [
                        {
                            "_key": "bd15635cc1ca",
                            "_type": "span",
                            "marks": [],
                            "text": ""
                        }
                    ],
                    "markDefs": [],
                    "style": "normal"
                }
            ],
            "categories": [],
            "publishedAt": "2022-11-09T18:10:00.000Z",
            "slug": {
                "_type": "slug",
                "current": "first-post"
            },
            "title": "First Post"
        }
    ]
}

v3.1.0 has broken React Native apps without `Intl` support

#2 introduced a new dependency event-source-polyfill, which uses the Intl API in one place. Whilst this API is present in most browser implementations, some JavaScript environments do not include support for Intl out of the box - for example, React Native.

I don't know if this package is intended for environments such as React Native? If so, there should maybe be an item in the documentation mentioning that a polyfill for Intl is needed here too. It does however seem a shame that a single line in a third party library can bring down an app, and require a large polyfill...

document publishing

Hi, we have been using the client to create and update documents but when updating a document it appears to create a draft version. Is it possible to trigger the publish function through the client call?

Sanity CMS with React Native not updating app contents immediately.

Hey everyone, I'm using Sanity CMS as a backend for my React Native application, but I come across a problem i.e. sanity client not updating app contents whenever a change is made from the app.

For eg. whenever a post is created from the app it is immediately created in the sanity desk but it takes too long to update it on the app and it is same in case of deletion of a post. It takes at least 45 seconds or a minute to update the app contents. Is there any way to get the data from sanity and update the app contents as soon as the post is created, updated or deleted in the sanity desk ?

PS: I've already used useCdn: true and sanityClient.listen() in my code, but there is no change I could see in the app's behavior.

Unable to use sanityClient on ReactNative (Expo) projects

Describe the bug

Unable to fetch data from Sanity CMS with React Native project using

import { createClient } from "@sanity/client";

Error: URLSearchParams.set is not implemented

To Reproduce

Steps to reproduce the behavior:

sanity.js [/sanity.js]

import { createClient } from "@sanity/client";
import imageUrlBuilder from "@sanity/image-url";

const client = createClient({
    projectId: "siovyk4p",
    dataset: "production",
    useCdn: false,
    apiVersion: "2021-10-21",
 })

const builder = imageUrlBuilder(client);
export const urlFor = (source) => builder.image(source);

export default client;

HomeScrn.js [/screen/HomeScrn.js]

import sanityClient from '../sanity';
import { useState } from 'react';

const [post, setPost] = useState([]);
const query = `*[_type == "post"] {...}`;

useEffect(() => {
sanityClient.fetch(encodeURI(query)).then ((data) => {setPost(data) });
},[]

Expected behavior

Able to Fetch the data from SanityCMS

Which versions of Sanity are you using?

@sanity/cli (global) 3.8.3 (up to date)
@sanity/eslint-config-studio 2.0.1 (up to date)
@sanity/vision 3.8.3 (up to date)
sanity 3.8.3 (up to date)

What operating system are you using?

MacOS Monterey v 12.0.1

Which versions of Node.js / npm are you running?

9.5.0

Additional context

N/A

Webpack error with expo

Describe the bug

Trying to launch project in web browser using Sanity and React-Native. However, I'm coming across these errors:

./node_modules/@sanity/client/dist/sanityClient.browser.mjs 2569:2-13
Can't import the named export 'jsonRequest' from non EcmaScript module (only default export is available)
./node_modules/@sanity/client/dist/sanityClient.browser.mjs 2570:2-14
Can't import the named export 'jsonResponse' from non EcmaScript module (only default export is available)
./node_modules/@sanity/client/dist/sanityClient.browser.mjs 2573:2-13
Can't import the named export 'observable' from non EcmaScript module (only default export is available)
./node_modules/@sanity/client/dist/sanityClient.browser.mjs 2571:2-10
Can't import the named export 'progress' from non EcmaScript module (only default export is available)

To Reproduce

  1. Create React Native project
  2. Install Sanity using npm i sanity
  3. Launch expo using expo start
  4. Launch web browser project with w

Expected behavior

A web browser with your React Native project should appear.

Screenshots
If applicable, add screenshots to help explain your problem.

Which versions of Sanity are you using?
locally:

@sanity/cli (global)  3.2.4 (up to date)
@sanity/image-url     1.0.2 (up to date)

In my package-lock.json
"@sanity/client": "version": "4.0.1"

What operating system are you using?
Mac OSX Ventura 13.0

Which versions of Node.js / npm are you running?

Run npm -v && node -v in the terminal and copy-paste the result here.

8.15.0
v16.17.1

"expected '}' following object body"

When i query *[_type=="post"] i get this responsemessage : "expected '}' following object body" name : "ClientError"
sanity client

export default sanityClient({
  projectId: "id", // find this at manage.sanity.io or in your sanity.json
  dataset: "production", // this is from those question during 'sanity init'
  useCdn: true,
  apiVersion: "2021-03-25",
});

versions
@sanity/base 2.32.0 → 2.33.0 @sanity/default-layout 2.32.0 → 2.33.0 @sanity/default-login 2.32.0 → 2.33.0 @sanity/desk-tool 2.32.0 → 2.33.0 @sanity/vision 2.32.0 → 2.33.0

MutationSelection types doesn't match documentation

According to the documentation on deletion here one should be able to do this:

client
  .delete({query: '*[_type == $type][0..1]', params: {type: 'bike'}})
  .then(() => {
    console.log('The documents matching *[_type == "bike"][0..1] was deleted')
  })
  .catch((err) => {
    console.error('Delete failed: ', err.message)
  })

But the MutationSelection type, which is used by delete, is type MutationSelection = {query: string} | {id: string} , and therefore errors out.

Depending on where that type is used it should be as simple as:

type MutationSelection = {query: string} | {query: string, params: QueryParams } | {id: string}
// or
type MutationSelection = {query: string, params?: QueryParams} | {id: string}

unexpected EOF

I quite often use the functionality associated with importing and exporting a dataset, I used this functionality sometimes once a month, and sometimes more often, but yesterday I encountered the following problem: when I try to import a dataset, I get a strange “unexpected EOF” error. This error is reproduced for me on different versions of node, different operating systems, as well as on different versions of this package, the process always breaks at 4%, once when I asked a colleague to reproduce, his process stopped at 5%. I would really appreciate it if you could provide any recommendations on how to fix this.
image

Delete using query inside transaction is missing support

According to the documentation (https://www.sanity.io/docs/http-mutations#d8ebd1878516l) one should be able to pass a query to delete inside a transaction:

{
  "mutations": [
    {
      "delete": {
        "query": "*[_type == 'feature' && viewCount < $views]",
        "params": {
          "views": 5
        },
      }
    }
  ]
}

But doing the following gives an error:

    const transaction = new sanityClient.Transaction()
    transaction.delete({query: '*[_type == "apartment"]'})
    //... other operations
    accountClient.mutate(transaction)
Error: delete(): "[object Object]" is not a valid document ID
    at Object.exports.validateDocumentId (node_modules/@sanity/client/lib/validators.js:42:11)
    at Transaction._delete [as delete] (node_modules/@sanity/client/lib/data/transaction.js:47:16)
    at Object.importApartmentsTo (/app/netlify/src/provider/accounts.js:45:29)
    at module.exports (app/netlify/src/usecase/handleSingleAccountImport.js:11:18)
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
    at async Object.exports.handler (app/netlify/functions/fetch-account.js:19:5)

node_modules/@sanity/client/lib/data/transaction.js:46:

  delete: function _delete(documentId) {
    validators.validateDocumentId('delete', documentId);
    return this._add({
      delete: {
        id: documentId
      }
    });
  },

I'm using @sanity/client 3.4.1.
is there a way to work around it?

Sanity Node JS client typescript error for ObservableTransaction Type

Hi,
I'm trying to compile a typescript node js project and sanity IO client throws these two errors:

"Property 'commit' in type 'ObservableTransaction' is not assignable to the same property in base type 'Transaction' "
"Property 'clone' in type 'ObservablePatch' is not assignable to the same property in base type 'Patch' "

`node_modules/@sanity/client/sanityClient.d.ts:387:3 - error TS2416: Property 'clone' in type 'ObservablePatch' is not assignable to the same property in base type 'Patch'.
Type '() => ObservablePatch' is not assignable to type '() => Patch'.
Call signature return types 'ObservablePatch' and 'Patch' are incompatible.
The types returned by 'commit(...)' are incompatible between these types.
Type 'Observable<SanityDocument>' is missing the following properties from type 'Promise<SanityDocument>': then, catch, finally, [Symbol.toStringTag]

387 clone(): ObservablePatch`

`node_modules/@sanity/client/sanityClient.d.ts:560:3 - error TS2416: Property 'commit' in type 'ObservableTransaction' is not assignable to the same property in base type 'Transaction'.
Type '{ (options: TransactionFirstDocumentMutationOptions): Observable<SanityDocument>; (options: TransactionAllDocumentsMutationOptions): Observable<...>; (options: TransactionFirstDocumentIdMutationOptions): Observable<...>; (options: TransactionAllDocumentIdsMutationOptions): Observable<...>; (options?: BaseMu...' is not assignable to type '{ (options:
TransactionFirstDocumentMutationOptions): Promise<SanityDocument>; (options: TransactionAllDocumentsMutationOptions): Promise<...>; (options: TransactionFirstDocumentIdMutationOptions): Promise<...>; (options: TransactionAllDocumentIdsMutationOptions): Promise<...>; (options?: BaseMutationOption...'.

560 commit(options: TransactionFirstDocumentIdMutationOptions): Observable`

and this is my tsconfig.json:
{ "compilerOptions": { "target": "es6", "lib": ["es2017", "esnext.asynciterable", "es2019", "ES2020.Promise"], "typeRoots": ["./node_modules/@types"], "allowSyntheticDefaultImports": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, "forceConsistentCasingInFileNames": true, "moduleResolution": "node", "module": "commonjs", "pretty": true, "sourceMap": true, "outDir": "./dist", "baseUrl": "./src", "rootDir": ".", "allowJs": true, "noEmit": false, "esModuleInterop": true, "incremental": true }, "include": ["./src/**/*", "./types"], "exclude": ["node_modules", "tests"] }

Type incompatibility with `nodenext` module resolution

I'm attempting to update some of my TypeScript codebases to use the named exports functionality in package.json. There seems to be an issue with the type definitions for the client when TypeScript is configured to use nodenext module resolution. The issue seems to stem from the final export default SanityClientConstructor line in the sanityClient.d.ts file:

export default SanityClientConstructor
. Since the client is not marked as a module in package.json, TypeScript parses it as a CommonJS file. Given that export default is not valid CJS syntax, TypeScript chokes up on it and fails to properly infer types - this is by design on TypeScript's part I believe.

Correcting the export to the CJS style export = SanityClientConstructor seems to solve this problem, although I haven't yet tested for any regression errors. Should I open a PR to look into correcting this?

Changelog not updated

It would be great if we could get a changelog update every time a new version is tagged/released.

Can't import the named export 'jsonRequest' from non EcmaScript module (only default export is available)

Hello i am receiving this error:


./node_modules/@sanity/client/dist/sanityClient.browser.mjs
Can't import the named export 'jsonRequest' from non EcmaScript module (only default export is available)

here is my client.js:


import SanityClient from '@sanity/client';
import imageUrlBuilder from '@sanity/image-url';

export const client = SanityClient({
    projectId: process.env.REACT_APP_SANITY_PROJECT_ID,
    dataset: 'production',
    apiVersion:'2021-10-21',
    useCdn: false,
    token: process.env.NEXT_PUBLIC_SANITY_TOKEN,
});

const builder = imageUrlBuilder(client);

export const urlFor = (source) => builder.image(source);


whenever this export is imported in any file i get this error.

import {client} from '../client'

i have tried sanityClient, SanityClient and createClient all resulting in the same issue.

finally here is my package.json and a screenshot of the manager on sanity:

{
  "name": "excuse_frontend",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@react-oauth/google": "^0.7.0",
    "@sanity/client": "^4.0.1",
    "@sanity/image-url": "^1.0.2",
    "@testing-library/jest-dom": "^5.16.5",
    "@testing-library/react": "^13.4.0",
    "@testing-library/user-event": "^13.5.0",
    "axios": "^1.3.3",
    "jwt-decode": "^3.1.2",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-icons": "^4.7.1",
    "react-loader-spinner": "^5.3.4",
    "react-masonry-css": "^1.0.16",
    "react-router-dom": "^6.8.1",
    "react-scripts": "^2.1.3",
    "uuid": "^9.0.0",
    "web-vitals": "^2.1.4"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },
  "devDependencies": {
    "tailwindcss": "^3.2.6"
  }
}

Screenshot 2023-02-14 at 3 43 54 AM

Any solutions? I can't find any proper solutions online.
Much appreciated.

What should I do when listener connection lifetime ends?

Currently Sanity has a 30 minutes limit for the listener connection lifetime.

Does it mean that I should rerun client.listen(myquery) every 30 minutes to make sure the user doesn't see a stale version of the dataset?

Should I inform the user?

is the library doing something to avoid presenting stale data?

See Sanity documentation.

Type errors

The ObservableTransaction and ObservablePatch types are no longer correct. Projects with skipLibCheck: false will likely not compile when using the client.

There are noe test or compile errors, but you get squiggles in the editor.

Property 'clone' in type 'ObservablePatch' is not assignable to the same property in base type 'Patch'.

Translated error

Screenshot 2022-08-15 at 10 46 36

`withCredentials` not working in Safari

We are currently running into Problems with a Nuxt application that has a custom preview mode with a query flag implemented for preview urls coming from a Sanity Studio deployment.

It seems like the withCredentials option is simply not working in Safari (tested on MacOs, Safari 16.1 and iOS 16.3.1). If I manually pass a token, the preview works as expected. The previews also work in Chrome.

It seems like Sanity Studio usually creates a sanitySession cookie with the token, whereas in Safari it creates a __studio_auth_token_${PROJECT_ID} entry in localStorage. Not sure if this is related, but might be the problem?

Would be great if this could be debugged, let me know if I can provide any other information!

Dependency Dashboard

This issue lists Renovate updates and detected dependencies. Read the Dependency Dashboard docs to learn more.


Using a curated preset maintained by


Sanity: The Composable Content Cloud

Pending Approval

These branches will be created by Renovate only once you click their checkbox below.

  • chore(deps): update non-major (@edge-runtime/types, @edge-runtime/vm, @sanity/pkg-utils, rollup, terser)
  • fix(deps): update dependency get-it to ^8.6.5
  • chore(deps): update dependency rimraf to v6
  • 🔐 Create all pending approval PRs at once 🔐

Awaiting Schedule

These updates are awaiting their schedule. Click on a checkbox to get an update now.

  • chore(deps): lock file maintenance

Ignored or Blocked

These are blocked by an existing closed PR and will not be recreated unless you click a checkbox below.

Detected dependencies

bun
package.json
  • @sanity/eventsource ^5.0.2
  • get-it ^8.6.4
  • rxjs ^7.0.0
  • @edge-runtime/types ^3.0.1
  • @edge-runtime/vm ^4.0.1
  • @rollup/plugin-commonjs ^26.0.1
  • @rollup/plugin-node-resolve ^15.2.3
  • @sanity/pkg-utils ^6.10.9
  • @types/json-diff ^1.0.3
  • @typescript-eslint/eslint-plugin ^7.18.0
  • @typescript-eslint/parser ^7.18.0
  • @vercel/stega 0.1.2
  • @vitest/coverage-v8 2.0.5
  • eslint ^8.57.0
  • eslint-config-prettier ^9.1.0
  • eslint-plugin-prettier ^5.2.1
  • eslint-plugin-simple-import-sort ^12.1.1
  • faucet ^0.0.4
  • happy-dom ^12.10.3
  • json-diff ^1.0.6
  • ls-engines ^0.9.3
  • next ^14.2.5
  • nock ^13.5.4
  • prettier ^3.3.3
  • prettier-plugin-packagejson ^2.5.1
  • rimraf ^5.0.7
  • rollup ^4.20.0
  • sse-channel ^4.0.0
  • terser ^5.31.5
  • typescript 5.5.3
  • vitest 2.0.5
  • vitest-github-actions-reporter 0.11.1
  • node >=14.18
github-actions
.github/workflows/browserslist.yml
  • actions/checkout v4
  • actions/setup-node v4
  • actions/create-github-app-token v1
  • peter-evans/create-pull-request v6@c5a7806660adbe173f04e3e038b0ccdcd758773c
.github/workflows/bun.yml
  • actions/checkout v4
  • oven-sh/setup-bun v2@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5
  • actions/create-github-app-token v1
  • peter-evans/create-pull-request v6@c5a7806660adbe173f04e3e038b0ccdcd758773c
.github/workflows/ci.yml
  • actions/checkout v4
  • actions/setup-node v4
  • actions/upload-artifact v3
  • actions/checkout v4
  • actions/setup-node v4
  • actions/checkout v4
  • actions/cache v4
  • actions/setup-node v4
  • actions/checkout v4
  • actions/setup-node v4
  • actions/download-artifact v3
  • actions/checkout v4
  • actions/setup-node v4
  • actions/download-artifact v3
  • actions/checkout v4
  • actions/cache v4
  • actions/download-artifact v3
  • denoland/setup-deno v1
  • actions/checkout v4
  • actions/cache v4
  • actions/download-artifact v3
  • antongolub/action-setup-bun v1@f0b9f339a7ce9ba1174a58484e4dc9bbd6f7b133
  • actions/checkout v4
  • actions/cache v4
  • actions/setup-node v4
  • actions/download-artifact v3
  • actions/checkout v4
  • actions/setup-node v4
.github/workflows/deno.yml
  • actions/checkout v4
  • actions/setup-node v4
  • denoland/setup-deno v1
  • actions/create-github-app-token v1
  • peter-evans/create-pull-request v6@c5a7806660adbe173f04e3e038b0ccdcd758773c
.github/workflows/lock.yml
  • dessant/lock-threads v5@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771
.github/workflows/prettier.yml
  • actions/checkout v4
  • actions/setup-node v4
  • actions/cache v4
  • actions/create-github-app-token v1
  • peter-evans/create-pull-request v6@c5a7806660adbe173f04e3e038b0ccdcd758773c
.github/workflows/release-please.yml
  • actions/create-github-app-token v1
  • google-github-actions/release-please-action v4
  • actions/checkout v4
  • actions/setup-node v4
npm
package.json
  • @sanity/eventsource ^5.0.2
  • get-it ^8.6.4
  • rxjs ^7.0.0
  • @edge-runtime/types ^3.0.1
  • @edge-runtime/vm ^4.0.1
  • @rollup/plugin-commonjs ^26.0.1
  • @rollup/plugin-node-resolve ^15.2.3
  • @sanity/pkg-utils ^6.10.9
  • @types/json-diff ^1.0.3
  • @typescript-eslint/eslint-plugin ^7.18.0
  • @typescript-eslint/parser ^7.18.0
  • @vercel/stega 0.1.2
  • @vitest/coverage-v8 2.0.5
  • eslint ^8.57.0
  • eslint-config-prettier ^9.1.0
  • eslint-plugin-prettier ^5.2.1
  • eslint-plugin-simple-import-sort ^12.1.1
  • faucet ^0.0.4
  • happy-dom ^12.10.3
  • json-diff ^1.0.6
  • ls-engines ^0.9.3
  • next ^14.2.5
  • nock ^13.5.4
  • prettier ^3.3.3
  • prettier-plugin-packagejson ^2.5.1
  • rimraf ^5.0.7
  • rollup ^4.20.0
  • sse-channel ^4.0.0
  • terser ^5.31.5
  • typescript 5.5.3
  • vitest 2.0.5
  • vitest-github-actions-reporter 0.11.1
  • node >=14.18

  • Check this box to trigger a request for Renovate to run again on this repository

webpack SSR bundle failures with node:

v5.4.2 of client is throwing these errors during build for a gatsby 5.9.0 site React 18. Not sure when the issue occurs but downgrading to latest v4 (v4.0.1) and it goes away. Tried polyfilling it in webpack config but list just keeps growing.

 ERROR #98123  WEBPACK.BUILD-HTML

Generating SSR bundle failed

Reading from "node:https" is not handled by plugins (Unhandled scheme).
Webpack supports "data:" and "file:" URIs by default.
You may need an additional plugin to handle "node:" URIs.

File: node:https


 ERROR #98123  WEBPACK.BUILD-HTML

Generating SSR bundle failed

Reading from "node:http" is not handled by plugins (Unhandled scheme).
Webpack supports "data:" and "file:" URIs by default.
You may need an additional plugin to handle "node:" URIs.

File: node:http


 ERROR #98123  WEBPACK.BUILD-HTML

Generating SSR bundle failed

Reading from "node:url" is not handled by plugins (Unhandled scheme).
Webpack supports "data:" and "file:" URIs by default.
You may need an additional plugin to handle "node:" URIs.

File: node:url

Fix Issue with "url.origin is not implemented" Error in React Native V6.4.4

Problem Description

When using the encodeQueryString function in the React Native environment, we are encountering the error message: "url.origin is not implemented." It appears that this issue is related to the use of URLSearchParams, which is not fully supported in React Native.

Proposed Solution

To address this problem, I suggest refactoring the encodeQueryString function to manually construct the query string without relying on URLSearchParams. This will ensure compatibility with React Native as well as other JavaScript environments.

Here's a snippet of the proposed changes:

import type { Any, QueryParams } from '../types';

export const encodeQueryString = ({
  query,
  params = {},
  options = {},
}: {
  query: string;
  params?: QueryParams;
  options?: Any;
}) => {
  // We generally want tag at the start of the query string
  const { tag, ...opts } = options;
  let searchParams = '';

  // We're manually appending the 'tag' parameter if it exists
  if (tag) searchParams += `tag=${encodeURIComponent(tag)}&`;
  searchParams += `query=${encodeURIComponent(query)}`;

  // Iterate params, the keys are prefixed with `$` and their values JSON stringified
  for (const [key, value] of Object.entries(params)) {
    searchParams += `&$${encodeURIComponent(key)}=${encodeURIComponent(JSON.stringify(value))}`;
  }
  // Options are passed as-is
  for (const [key, value] of Object.entries(opts)) {
    // Skip falsy values
    if (value) searchParams += `&${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
  }

  return `?${searchParams}`;
};

API permission problems fail silently

If a dataset is set to private and no/empty API key is given to createClient, the query will silently return null instead of erroring which is confusing and makes it hard to track down the problem, especially for new users.

For example, in the following case it's possible for SANITY_API_TOKEN to be undefined in the environment:

const client = createClient({
  projectId: 'xxxxxxx',
  dataset: 'production',
  apiVersion: '2022-10-21',
  useCdn: false,
  token: process.env.SANITY_API_TOKEN
})

If a query is made, the result will be null:

const result = await client.fetch(groq`
 *[_type == "somePage"]
`)
console.log(result)
// null

I understand it might be a security concern to report a failure to access a project or dataset, but on a recent project I've wasted quite a lot of time tracking the issue down, so a hint would be good. Maybe an error if undefined or null is explicitly passed to createClient?

Add issue template

When opening #158, I expected to see an issue template guiding me around opening an issue here and potentially asking for specific information the maintainers might need to reproduce my problem or provide a fix

cannotModifyImmutableAttributeError should give me document id

Is your feature request related to a problem? Please describe.

Using the JavaScript client, I create a transaction and push a lot of documents with createOrReplace(). Sometimes I get a Cannot modify immutable attribute "type" error from .commit(), and while I understand why it happens, the error does not have enough detail to tell me which of the many documents is causing the problem. Could this error object be extended a bit to add a documentId property or similar?

Describe the solution you'd like

    "error": {
      "attribute": "_type",
      "description": "Cannot modify immutable attribute \"_type\"",
      "type": "cannotModifyImmutableAttributeError",
      "documentId": "fooo_id"
    }

Uploading an image with a filename specified always results in the last character of the filename being removed in the browser.

Describe the bug

Trying to upload an image asset with a filename specified always results in the last character of the filename being removed in the browser.

To Reproduce

Upload an image using Sanity.client.upload()

const fileUpload = await client.assets.upload('image', pngBuffer, {
  contentType: 'image/png',
  filename: 'myFile',
})

console.log('The file was uploaded!', fileUpload)

Expected behavior

I expect to see the filename 'myFile' in the browser under the media details, but I see 'myFil'.
Oddly enough in the media collection view I do see the correct filename.
I also see the correct filename in the request under the originalFilename key.
Please see screenshots.

Screenshots
Filename with last character removed
Screen Shot 2023-02-01 at 1 00 46 PM

Filename showing as specified
Screen Shot 2023-02-01 at 1 03 54 PM

Correct name in the request under the originalFile key
Screen Shot 2023-02-01 at 1 06 17 PM

Which versions of Sanity are you using?

"@sanity/client": "^3.4.1"

What operating system are you using?

Mac OS Monterey 12.1

Which versions of Node.js / npm are you running?

8.19.3
v16.19.0

How to create a document with a slug

I am trying to create a "redirect" document through the sanity client that also includes a slug field. I've been using the code snippet below to create a slug:

const document = {
  _type: "redirect",
  name: "test",
  slug: {
    _type: "slug",
    current: "test",
  },
  url: "test.com",
};

await client.create(document);

This code snippet is able to successfully create a redirect document with no apparent error
image
but the the client still raises a "slug is not defined" reference error.

Would love some guidance on how to move forward.

`@sanity/client` from file

While trying to resolve module @sanity/client from file C:\Users\musar\Downloads\hit-400-app\sanity.js, the package C:\Users\musar\Downloads\hit-400-app\node_modules\@sanity\client\package.json was successfully found. However, this package itself specifies a main module field that could not be resolved (C:\Users\musar\Downloads\hit-400-app\node_modules\@sanity\client\dist\index.browser.cjs. Indeed, none of these files exist:

"window is not defined" using sanity client on sveltekit while deploying to cloudflare pages + workers

Describe the bug

I'm unable to deploy to Cloudflare Workers while using @sanity/client and SvelteKit

To Reproduce

Create an empty SvelteKit project with TS, hook it up with sanity client and Cloudflare by creating the following files.

// src/lib/client.js
import sanityClient from '@sanity/client';

export default sanityClient({
	projectId: 'xxxxxx',
	dataset: 'production',
	useCdn: true,
	apiVersion: 'v2021-08-02'
});
// src/routes/blog.svelte
<script context="module">
	import client from '$lib/client';

	export async function load() {
		const query = `*[_type == "post"]{
			title
			mainImage
		}`;
		const posts = await client.fetch(query);

		return {
			props: { posts }
		};
	}
</script>

<script lang="ts">
	import imageUrlBuilder from '@sanity/image-url';

	export let posts;

	const builder = imageUrlBuilder(client);

	function urlFor(source) {
		return builder.image(source);
	}
</script>

<div>
	{#each posts as { title, mainImage}}
		<h2>{title}</h2>
		<img src={urlFor(mainImage).width(400).url()} />
	{:else}
		<div>No posts yet</div>
	{/each}
</div>
// svelte.config.js
import preprocess from 'svelte-preprocess';
import cfworkers from '@sveltejs/adapter-cloudflare-workers';

/** @type {import('@sveltejs/kit').Config} */
const config = {
	preprocess: preprocess(),
	kit: {
		target: '#svelte',
		adapter: cfworkers()
	}
};

export default config;

Run the following to set up the config for deployment

wrangler init --site my-site-name
# wrangler.toml
name = "site name here"
type = "javascript"
route = ''
account_id = "enter your account id here"
zone_id = 'enter your zone id here if you have a domain with cloudflare'
usage_model = ''
compatibility_flags = []
workers_dev = true
compatibility_date = "2021-10-29"

[site]
bucket = "./build"
entry-point = "workers-site"

[build]
command = ""

[build.upload]
format = "service-worker"

Then run the following to build the project:

yarn build

Finally run the following for dev or directly deploy:

wrangler dev

or

wrangler publish

Either of these commands will return:

up to date, audited 1 package in 373ms

found 0 vulnerabilities
🌀  Using namespace for Workers Site "__site-name-workers_sites_assets_preview"
✨  Success
Error: Something went wrong with the request to Cloudflare...
Uncaught ReferenceError: window is not defined
  at line 1 in node_modules/.pnpm/@[email protected]/node_modules/@sanity/eventsource/browser.js
  at line 1
  at line 1 in node_modules/.pnpm/@[email protected]/node_modules/@sanity/client/lib/data/listen.js
  at line 1
  at line 1 in node_modules/.pnpm/@[email protected]/node_modules/@sanity/client/lib/data/dataMethods.js
  at line 1
  at line 1 in node_modules/.pnpm/@[email protected]/node_modules/@sanity/client/lib/sanityClient.js
  at line 1
  at line 1
  at line 1
 [API code: 10021]

Expected behavior

Successful deployment in Cloudflare Workers with a SvelteKit project

Which versions of Sanity are you using?

These are the packages in question

"@sanity/client": "^2.21.7",
"@sanity/image-url": "^0.140.22",
"@sveltejs/adapter-cloudflare-workers": "^1.0.0-next.25",
"@sveltejs/kit": "^1.0.0-next.192",
"svelte": "^3.44.0",
"svelte-check": "^2.2.7",
"svelte-preprocess": "^4.9.8",
"tslib": "^2.3.1",
"typescript": "^4.4.4"

What operating system are you using?

MacOS Big Sur 11.6 (20G165)

Which versions of Node.js / npm are you running?

❯ yarn --version
1.22.15
❯ npm --version
8.0.0
❯ node --version
v16.11.0

Configure client to only pull back published data

I'm using an authenticated dataset. So when I pull back data it includes both drafts and published documents.

I would like to have an option for the client to only pull back published documents.

This means in the preview mode it can pull back everything (how it currently behaves) and in normal mode it will only use the published documents.

Currently, the way to achieve this is to add it to the queries but this adds extra complexity: https://www.sanity.io/docs/drafts#ebfb408fa9b6

The current workaround is to append slug.current == $slug && (!$publishedOnly || !(_id in path('drafts.**'))) to each of my document selectors which isn't very nice:

export const pageBySlugQuery = groq`
  *[_type == "page" && slug.current == $slug && (!$publishedOnly || !(_id in path('drafts.**')))][0]
`

// Then use params: { publishedOnly: true }

Odd error during `next build`

Using:

"next": "13.5.2",
"next-sanity": "^5.5.4",
"@sanity/client": "^6.4.12",

When I run next build I get the below:

./node_modules/@sanity/client/src/SanityClient.ts:60:3
Type error: Property '#clientConfig' has no initializer and is not definitely assigned in the constructor.

58 | * Private properties
59 | */

60 | #clientConfig: InitializedClientConfig

Error with client using Vercel Edge Function

Vercel Edge Functions are still in beta but that sounds like a Sanity issue here: I'm running a SvelteKit project using the Sanity Client to make queries on Page Endpoints through a function, and this is the error that I get from the Vercel logs:

Error: Dynamic require of "url" is not supported
    at worker.js:274:21872
    at node_modules/.pnpm/[email protected]/node_modules/eventsource/lib/eventsource.js (worker.js:324:7541)
    at worker.js:274:22035
    at node_modules/.pnpm/@[email protected]/node_modules/@sanity/eventsource/node.js (worker.js:325:1640)
    at worker.js:274:22035
    at node_modules/.pnpm/@[email protected]/node_modules/@sanity/client/lib/data/listen.js (worker.js:325:2969)
    at worker.js:274:22035
    at node_modules/.pnpm/@[email protected]/node_modules/@sanity/client/lib/data/dataMethods.js (worker.js:325:5186)
    at worker.js:274:22035
    at node_modules/.pnpm/@[email protected]/node_modules/@sanity/client/lib/sanityClient.js (worker.js:331:24738)

More infos: https://vercel.com/docs/concepts/functions/edge-functions
Possible limitations (Node related?): https://vercel.com/docs/concepts/functions/edge-functions/middleware-api#unsupported-apis-and-runtime-restrictions

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.