Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/pr-1304.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<!-- auto-generated -->
---
'@sanity/client': major

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lol clanker, we are not doing a major for this

---

fix!: throw when querying drafts perspective with the API-CDN
21 changes: 21 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,27 @@ export function validateApiPerspective(
}
}

/**
* Gradient rejects `drafts` and `previewDrafts` on the API-CDN. Stacked
* perspectives that include those values are treated the same. Other stacked
* perspectives still fall back to the Live API with a warning.
*
* @internal
*/
export function perspectiveConflictsWithCdn(perspective: ClientPerspective): boolean {
if (perspective === 'previewDrafts' || perspective === 'drafts') {
return true
}
if (Array.isArray(perspective)) {
return perspective.includes('drafts') || perspective.includes('previewDrafts')
}
return false
}

/** @internal */
export const CDN_INCOMPATIBLE_PERSPECTIVE_ERROR =
'The Sanity client is configured with the `perspective` set to `drafts` or `previewDrafts`, which does not support the API-CDN. Set `useCdn: false`.'

export const initConfig = (
config: Partial<ClientConfig>,
prevConfig: Partial<ClientConfig>,
Expand Down
22 changes: 11 additions & 11 deletions src/data/dataMethods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import {getDraftId, getVersionFromId, getVersionId, isDraftId} from '@sanity/cli
import {type MonoTypeOperatorFunction, Observable} from 'rxjs'
import {filter, map} from 'rxjs/operators'

import {validateApiPerspective} from '../config'
import {
CDN_INCOMPATIBLE_PERSPECTIVE_ERROR,
perspectiveConflictsWithCdn,
validateApiPerspective,
} from '../config'
import {type FetchRequest, requestOptions} from '../http/requestOptions'
import type {ObservableSanityClient, SanityClient} from '../SanityClient'
import {stegaClean, type StegaCleaned} from '../stega/stegaClean'
Expand Down Expand Up @@ -40,7 +44,7 @@ import {getSelection} from '../util/getSelection'
import * as validate from '../validators'
import * as validators from '../validators'
import {
printCdnPreviewDraftsWarning,
printCdnStackedPerspectiveWarning,
printCreateVersionWithBaseIdWarning,
printDeprecatedUriOptionWarning,
printPreviewDraftsDeprecationWarning,
Expand Down Expand Up @@ -1063,16 +1067,12 @@ export function _prepareRequest(client: Client, options: RequestObservableOption
: perspectiveOption,
...options.query,
}
// If the perspective is set to `drafts` or multiple perspectives we can't use the CDN, the API will throw
if (
((Array.isArray(perspectiveOption) && perspectiveOption.length > 0) ||
// previewDrafts was renamed to drafts, but keep for backwards compat
perspectiveOption === 'previewDrafts' ||
perspectiveOption === 'drafts') &&
useCdn
) {
if (useCdn && perspectiveConflictsWithCdn(perspectiveOption)) {
throw new Error(CDN_INCOMPATIBLE_PERSPECTIVE_ERROR)
}
if (useCdn && Array.isArray(perspectiveOption) && perspectiveOption.length > 0) {
useCdn = false
printCdnPreviewDraftsWarning()
printCdnStackedPerspectiveWarning()
}
}

Expand Down
6 changes: 5 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,11 @@ type ClientConfigResource =
export interface ClientConfig {
projectId?: string
dataset?: string
/** @defaultValue true */
/**
* Whether to use the API-CDN. `drafts` and `previewDrafts` cannot be queried
* through the CDN; combining them with `useCdn: true` throws.
* @defaultValue true
*/
useCdn?: boolean
token?: string

Expand Down
4 changes: 2 additions & 2 deletions src/warnings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ export const printCdnWarning = createWarningPrinter([
`\`useCdn: false\` to use the Live API. Note: You may incur higher costs using the live API.`,
])

export const printCdnPreviewDraftsWarning = createWarningPrinter([
`The Sanity client is configured with the \`perspective\` set to \`drafts\` or \`previewDrafts\`, which doesn't support the API-CDN.`,
export const printCdnStackedPerspectiveWarning = createWarningPrinter([
`The Sanity client is configured with a stacked \`perspective\`, which doesn't support the API-CDN.`,
`The Live API will be used instead. Set \`useCdn: false\` in your configuration to hide this warning.`,
])

Expand Down
205 changes: 205 additions & 0 deletions test/client/cdnPerspective.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
import {afterAll, beforeEach, describe, expect, test, vi} from 'vitest'

import {CDN_INCOMPATIBLE_PERSPECTIVE_ERROR} from '../../src/config'
import {getActiveMock} from '../helpers/mockFetch'
import {createClient} from './helpers'

const liveHost = 'https://abc123.api.sanity.io'
const cdnHost = 'https://abc123.apicdn.sanity.io'
const draftsQueryPath = '/v1/data/query/foo?query=*&returnQuery=false&perspective=drafts'
const publishedQueryPath = '/v1/data/query/foo?query=*&returnQuery=false&perspective=published'
const rawQueryPath = '/v1/data/query/foo?query=*&returnQuery=false&perspective=raw'
const stackedQueryPath =
'/v1/data/query/foo?query=*&returnQuery=false&perspective=published%2Cdrafts'

describe('API-CDN and drafts perspective', () => {
const result = [{_id: 'njgNkngskjg', rating: 5}]
const warn = vi.spyOn(console, 'warn')
beforeEach(() => {
warn.mockReset()
})
afterAll(() => {
warn.mockRestore()
})

describe('after: allowed combinations still query', () => {
test('drafts with useCdn false uses the Live API', async () => {
getActiveMock()
.scope(liveHost)
.on('GET', draftsQueryPath)
.respond({status: 200, body: {ms: 123, result}})

const client = createClient({
projectId: 'abc123',
dataset: 'foo',
useCdn: false,
perspective: 'drafts',
})
expect(await client.fetch('*', {})).toEqual(result)
})

test('published with useCdn true uses the API-CDN', async () => {
getActiveMock()
.scope(cdnHost)
.on('GET', publishedQueryPath)
.respond({status: 200, body: {ms: 123, result}})

const client = createClient({
projectId: 'abc123',
dataset: 'foo',
useCdn: true,
perspective: 'published',
})
expect(await client.fetch('*', {})).toEqual(result)
})

test('raw with useCdn true uses the API-CDN', async () => {
getActiveMock()
.scope(cdnHost)
.on('GET', rawQueryPath)
.respond({status: 200, body: {ms: 123, result}})

const client = createClient({
projectId: 'abc123',
dataset: 'foo',
useCdn: true,
perspective: 'raw',
})
expect(await client.fetch('*', {})).toEqual(result)
})

test('drafts with useCdn true still queries when fetch overrides useCdn to false', async () => {
getActiveMock()
.scope(liveHost)
.on('GET', draftsQueryPath)
.respond({status: 200, body: {ms: 123, result}})

const client = createClient({
projectId: 'abc123',
dataset: 'foo',
useCdn: true,
perspective: 'drafts',
})
expect(await client.fetch('*', {}, {useCdn: false})).toEqual(result)
})

test('stacked perspectives without drafts still fall back to the Live API when useCdn is true', async () => {
getActiveMock()
.scope(liveHost)
.on('GET', '/v1/data/query/foo?query=*&returnQuery=false&perspective=published%2Crrel123')
.respond({status: 200, body: {ms: 123, result}})

const client = createClient({
projectId: 'abc123',
dataset: 'foo',
useCdn: true,
perspective: ['published', 'rrel123'],
})
expect(await client.fetch('*', {})).toEqual(result)
})

test('stacked perspectives with useCdn false use the Live API', async () => {
getActiveMock()
.scope(liveHost)
.on('GET', stackedQueryPath)
.respond({status: 200, body: {ms: 123, result}})

const client = createClient({
projectId: 'abc123',
dataset: 'foo',
useCdn: false,
perspective: ['published', 'drafts'],
})
expect(await client.fetch('*', {})).toEqual(result)
})
})

describe('after: incompatible combinations throw instead of warning', () => {
test('constructing the client with drafts and useCdn true does not throw', () => {
expect(() =>
createClient({
projectId: 'abc123',
dataset: 'foo',
useCdn: true,
perspective: 'drafts',
}),
).not.toThrow()
})

test('fetch throws when config has drafts and useCdn true', () => {
const client = createClient({
projectId: 'abc123',
dataset: 'foo',
useCdn: true,
perspective: 'drafts',
})
expect(() => client.fetch('*', {})).toThrow(CDN_INCOMPATIBLE_PERSPECTIVE_ERROR)
expect(warn).not.toHaveBeenCalledWith(
expect.stringContaining('The Live API will be used instead'),
)
})

test('fetch throws when config has previewDrafts and useCdn true', () => {
const client = createClient({
projectId: 'abc123',
dataset: 'foo',
useCdn: true,
perspective: 'previewDrafts',
})
expect(() => client.fetch('*', {})).toThrow(CDN_INCOMPATIBLE_PERSPECTIVE_ERROR)
})

test('fetch throws when drafts is combined with the default useCdn true', () => {
const client = createClient({
projectId: 'abc123',
dataset: 'foo',
perspective: 'drafts',
})
expect(() => client.fetch('*', {})).toThrow(CDN_INCOMPATIBLE_PERSPECTIVE_ERROR)
})

test('fetch throws when a drafts perspective override is used on a CDN client', () => {
const client = createClient({
projectId: 'abc123',
dataset: 'foo',
useCdn: true,
})
expect(() => client.fetch('*', {}, {perspective: 'drafts'})).toThrow(
CDN_INCOMPATIBLE_PERSPECTIVE_ERROR,
)
})

test('fetch throws when a previewDrafts perspective override is used on a CDN client', () => {
const client = createClient({
projectId: 'abc123',
dataset: 'foo',
useCdn: true,
})
expect(() => client.fetch('*', {}, {perspective: 'previewDrafts'})).toThrow(
CDN_INCOMPATIBLE_PERSPECTIVE_ERROR,
)
})

test('fetch throws when a stack including drafts is used with useCdn true', () => {
const client = createClient({
projectId: 'abc123',
dataset: 'foo',
useCdn: true,
perspective: ['published', 'drafts'],
})
expect(() => client.fetch('*', {})).toThrow(CDN_INCOMPATIBLE_PERSPECTIVE_ERROR)
})

test('fetch throws when useCdn is overridden to true on a drafts client', () => {
const client = createClient({
projectId: 'abc123',
dataset: 'foo',
useCdn: false,
perspective: 'drafts',
})
expect(() => client.fetch('*', {}, {useCdn: true})).toThrow(
CDN_INCOMPATIBLE_PERSPECTIVE_ERROR,
)
})
})
})
12 changes: 12 additions & 0 deletions test/client/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
import {firstValueFrom} from 'rxjs'
import {describe, expect, test} from 'vitest'

import {perspectiveConflictsWithCdn} from '../../src/config'
import {getActiveMock} from '../helpers/mockFetch'
import {apiHost, createClient, getClient, projectHost} from './helpers'

Expand Down Expand Up @@ -185,6 +186,17 @@ describe('base client', () => {
).toThrow(/Invalid API perspective/)
})

test('perspectiveConflictsWithCdn matches the perspectives Gradient rejects on the API-CDN', () => {
expect(perspectiveConflictsWithCdn('drafts')).toBe(true)
expect(perspectiveConflictsWithCdn('previewDrafts')).toBe(true)
expect(perspectiveConflictsWithCdn('published')).toBe(false)
expect(perspectiveConflictsWithCdn('raw')).toBe(false)
expect(perspectiveConflictsWithCdn(['published'])).toBe(false)
expect(perspectiveConflictsWithCdn(['drafts', 'published'])).toBe(true)
expect(perspectiveConflictsWithCdn(['previewDrafts'])).toBe(true)
expect(perspectiveConflictsWithCdn([])).toBe(false)
})

test('throws on invalid project ids', () => {
expect(() => createClient({projectId: '*foo*'})).toThrow(/projectId.*?can only contain/i)
})
Expand Down
41 changes: 0 additions & 41 deletions test/client/data.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,29 +330,6 @@ describe('data', () => {
expect(res[0].rating, 'data should match').toBe(5)
})

test('automatically useCdn false if perspective is previewDrafts', async () => {
getActiveMock()
.scope('https://abc123.api.sanity.io')
.on('GET', `/v1/data/query/foo?query=*&returnQuery=false&perspective=previewDrafts`)
.respond({
status: 200,
body: {
ms: 123,
result,
},
})

const client = createClient({
projectId: 'abc123',
dataset: 'foo',
useCdn: true,
perspective: 'previewDrafts',
})
const res = await client.fetch('*', {})
expect(res.length, 'length should match').toBe(1)
expect(res[0].rating, 'data should match').toBe(5)
})

test('can query for documents with resultSourceMap and perspective using the third client.fetch parameter', async () => {
getActiveMock()
.scope(projectHost())
Expand Down Expand Up @@ -398,24 +375,6 @@ describe('data', () => {
expect(res[0].rating, 'data should match').toBe(5)
})

test('setting a perspective previewDrafts override on client.fetch sets useCdn to false', async () => {
getActiveMock()
.scope('https://abc123.api.sanity.io')
.on('GET', `/v1/data/query/foo?query=*&returnQuery=false&perspective=previewDrafts`)
.respond({
status: 200,
body: {
ms: 123,
result,
},
})

const client = createClient({projectId: 'abc123', dataset: 'foo', useCdn: true})
const res = await client.fetch('*', {}, {perspective: 'previewDrafts'})
expect(res.length, 'length should match').toBe(1)
expect(res[0].rating, 'data should match').toBe(5)
})

test('can query with a variant id set in the client config', async () => {
getActiveMock()
.scope(projectHost())
Expand Down
Loading