Skip to content

Commit 880ef78

Browse files
committed
feat(views): when connections are set use the emulate endpoint
1 parent d814d09 commit 880ef78

File tree

4 files changed

+165
-11
lines changed

4 files changed

+165
-11
lines changed

src/data/dataMethods.ts

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,15 @@ export function _fetch<R, Q>(
106106
? {...opts, fetch: {cache, next}}
107107
: opts
108108

109-
const $request = _dataRequest(config, httpRequest, 'query', {query, params}, reqOpts)
109+
// By default, we use the /query endpoint
110+
// But if we're in dev mode and using a view then we use /emulate
111+
let endpoint = 'query'
112+
const experimentalResource = config['~experimental_resource']
113+
if (experimentalResource && experimentalResource.type === 'view' && experimentalResource.useEmulate) {
114+
endpoint = 'emulate'
115+
}
116+
117+
const $request = _dataRequest(config, httpRequest, endpoint, {query, params}, reqOpts)
110118
return stega.enabled
111119
? $request.pipe(
112120
combineLatestWith(
@@ -413,11 +421,13 @@ export function _dataRequest(
413421
const isMutation = endpoint === 'mutate'
414422
const isAction = endpoint === 'actions'
415423
const isQuery = endpoint === 'query'
424+
const isEmulate = endpoint === 'emulate'
416425

417426
// Check if the query string is within a configured threshold,
418427
// in which case we can use GET. Otherwise, use POST.
419-
const strQuery = isMutation || isAction ? '' : encodeQueryString(body)
420-
const useGet = !isMutation && !isAction && strQuery.length < getQuerySizeLimit
428+
// Emulate endpoint always uses POST
429+
const strQuery = isMutation || isAction || isEmulate ? '' : encodeQueryString(body)
430+
const useGet = !isMutation && !isAction && !isEmulate && strQuery.length < getQuerySizeLimit
421431
const stringQuery = useGet ? strQuery : ''
422432
const returnFirst = options.returnFirst
423433
const {timeout, token, tag, headers, returnQuery, lastLiveEventId, cacheMode} = options
@@ -439,7 +449,7 @@ export function _dataRequest(
439449
resultSourceMap: options.resultSourceMap,
440450
lastLiveEventId: Array.isArray(lastLiveEventId) ? lastLiveEventId[0] : lastLiveEventId,
441451
cacheMode: cacheMode,
442-
canUseCdn: isQuery,
452+
canUseCdn: isQuery || isEmulate,
443453
signal: options.signal,
444454
fetch: options.fetch,
445455
useAbortSignal: options.useAbortSignal,
@@ -501,6 +511,9 @@ const isQuery = (config: InitializedClientConfig, uri: string) =>
501511
const isViewQuery = (config: InitializedClientConfig, uri: string) =>
502512
hasDataConfig(config) && uri.startsWith(_getDataUrl(config, 'views'))
503513

514+
const isEmulate = (config: InitializedClientConfig, uri: string) =>
515+
hasDataConfig(config) && uri.startsWith(_getDataUrl(config, 'emulate'))
516+
504517
const isMutate = (config: InitializedClientConfig, uri: string) =>
505518
hasDataConfig(config) && uri.startsWith(_getDataUrl(config, 'mutate'))
506519

@@ -520,7 +533,8 @@ const isData = (config: InitializedClientConfig, uri: string) =>
520533
isDoc(config, uri) ||
521534
isListener(config, uri) ||
522535
isHistory(config, uri) ||
523-
isViewQuery(config, uri)
536+
isViewQuery(config, uri) ||
537+
isEmulate(config, uri)
524538

525539
/**
526540
* @internal
@@ -549,8 +563,8 @@ export function _requestObservable<R>(
549563
options.query = {tag: validate.requestTag(tag), ...options.query}
550564
}
551565

552-
// GROQ query-only parameters
553-
if (['GET', 'HEAD', 'POST'].indexOf(options.method || 'GET') >= 0 && isQuery(config, uri)) {
566+
// GROQ query-only parameters (applies to both query and emulate endpoints)
567+
if (['GET', 'HEAD', 'POST'].indexOf(options.method || 'GET') >= 0 && (isQuery(config, uri) || isEmulate(config, uri))) {
554568
const resultSourceMap = options.resultSourceMap ?? config.resultSourceMap
555569
if (resultSourceMap !== undefined && resultSourceMap !== false) {
556570
options.query = {resultSourceMap, ...options.query}
@@ -715,6 +729,11 @@ const resourceDataBase = (config: InitializedClientConfig): string => {
715729
return `/dashboards/${id}`
716730
}
717731
case 'view': {
732+
const emulate = config['~experimental_resource'].useEmulate;
733+
if (emulate) {
734+
return `/views/${id}/emulate`
735+
}
736+
718737
return `/views/${id}`
719738
}
720739
default:

src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ type ClientConfigResource =
7373
| {
7474
type: 'view'
7575
id: string
76+
useEmulate?: boolean
7677
}
7778

7879
/** @public */

src/views/index.ts

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,32 @@ import {_fetch} from '../data/dataMethods'
1212
import {initConfig} from '../config'
1313
import {defineHttpRequest} from '../http/request'
1414

15+
/**
16+
* Helper function to check if a view has any dataset connections
17+
* @internal
18+
*/
19+
function hasDatasetConnections(
20+
viewId: string,
21+
viewOverrides?: ViewOverride[],
22+
): boolean {
23+
if (!viewOverrides) return false
24+
25+
const viewOverride = viewOverrides.find(override => override.id === viewId)
26+
if (!viewOverride || !viewOverride.connections.length) return false
27+
28+
// Check if any connection has dataset resourceType
29+
return viewOverride.connections.some(conn => conn.resourceType === ViewResourceType.Dataset)
30+
}
31+
1532
/** @public */
1633
export interface ViewClientConfig extends Omit<ClientConfig, 'dataset' | 'projectId' | 'useCdn' | 'useProjectHostname'> {
1734
viewOverrides?: ViewOverride[]
35+
apiVersion: string
36+
}
37+
38+
/** @public */
39+
export enum ViewResourceType {
40+
Dataset = 'dataset',
1841
}
1942

2043
/** @public */
@@ -27,9 +50,8 @@ export type ViewOverride = {
2750
export type ViewConnectionOverride = {
2851
name: string,
2952
query: string,
30-
dataset: string,
31-
projectId: string,
32-
apiVersion: string,
53+
resourceType: ViewResourceType,
54+
resourceId: string,
3355
}
3456

3557
/**
@@ -119,11 +141,15 @@ export class ViewClient {
119141
params?: Q,
120142
options?: ViewQueryOptions,
121143
): Promise<RawQueryResponse<ClientReturn<G, R>> | ClientReturn<G, R>> {
144+
// Check if this view has dataset connections
145+
const useEmulateEndpoint = hasDatasetConnections(viewId, this.#config.viewOverrides)
146+
122147
const cfg = initConfig(
123148
{
124149
'~experimental_resource': {
125150
id: viewId,
126151
type: 'view',
152+
useEmulate: useEmulateEndpoint,
127153
},
128154
},
129155
this.#config,
@@ -187,11 +213,15 @@ export class ObservableViewClient {
187213
params?: Q,
188214
options?: ViewQueryOptions,
189215
): Observable<RawQueryResponse<ClientReturn<G, R>> | ClientReturn<G, R>> {
216+
// Check if this view has dataset connections
217+
const useEmulateEndpoint = hasDatasetConnections(viewId, this.#config.viewOverrides)
218+
190219
const cfg = initConfig(
191220
{
192221
'~experimental_resource': {
193222
id: viewId,
194223
type: 'view',
224+
useEmulate: useEmulateEndpoint,
195225
},
196226
},
197227
this.#config,

test/views/client.test.ts

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {describe, expect, test, afterEach} from 'vitest'
2-
import {createViewClient, ObservableViewClient, type ViewClientConfig} from '../../src/views'
2+
import {createViewClient, ObservableViewClient, type ViewClientConfig, ViewResourceType} from '../../src/views'
33

44
const apiHost = 'api.sanity.url'
55
const apicdnHost = 'apicdn.sanity.url'
@@ -194,6 +194,69 @@ describe('view client', async () => {
194194
const res = await client.fetch('vw404', '*')
195195
expect(res).toEqual(result)
196196
})
197+
198+
test.skipIf(isEdge)('uses emulate endpoint when dataset connections are detected', async () => {
199+
const configWithOverrides: ViewClientConfig = {
200+
...defaultConfig,
201+
viewOverrides: [
202+
{
203+
id: 'vw-dataset-test',
204+
connections: [
205+
{
206+
name: 'main',
207+
query: '*[_type == "document"]',
208+
resourceType: ViewResourceType.Dataset,
209+
resourceId: 'project123.dataset456',
210+
},
211+
],
212+
},
213+
],
214+
}
215+
const client = createViewClient(configWithOverrides)
216+
const result = [{_id: 'dataset-doc', title: 'Dataset Document'}]
217+
218+
// Mock the emulate endpoint (POST request)
219+
nock(`https://${apicdnHost}`)
220+
.post('/v2025-01-01/views/vw-dataset-test/emulate/emulate?returnQuery=false', {
221+
query: '*[_type == "test"]',
222+
params: {},
223+
// TODO: Add view connections configuration to the body
224+
// This should include the dataset connections from viewOverrides
225+
})
226+
.reply(200, {ms: 100, result})
227+
228+
const res = await client.fetch('vw-dataset-test', '*[_type == "test"]')
229+
expect(res).toEqual(result)
230+
})
231+
232+
test.skipIf(isEdge)('falls back to view endpoint when no override matches', async () => {
233+
const configWithOverrides: ViewClientConfig = {
234+
...defaultConfig,
235+
viewOverrides: [
236+
{
237+
id: 'vw-other',
238+
connections: [
239+
{
240+
name: 'main',
241+
query: '*[_type == "document"]',
242+
resourceType: ViewResourceType.Dataset,
243+
resourceId: 'project123.dataset456',
244+
},
245+
],
246+
},
247+
],
248+
}
249+
const client = createViewClient(configWithOverrides)
250+
const result = [{_id: 'view-doc', title: 'View Document'}]
251+
252+
// Mock the view endpoint (default behavior)
253+
nock(`https://${apicdnHost}`)
254+
.get('/v2025-01-01/views/vw-no-override/query?query=*%5B_type+%3D%3D+%22test%22%5D&returnQuery=false')
255+
.reply(200, {ms: 100, result})
256+
257+
const res = await client.fetch('vw-no-override', '*[_type == "test"]')
258+
expect(res).toEqual(result)
259+
})
197260
})
198261

199262
describe('observable client', () => {
@@ -381,5 +444,46 @@ describe('view client', async () => {
381444
})
382445
})
383446
})
447+
448+
test.skipIf(isEdge)('observable client uses emulate endpoint when dataset connections are detected', async () => {
449+
const configWithOverrides: ViewClientConfig = {
450+
...defaultConfig,
451+
viewOverrides: [
452+
{
453+
id: 'vw-obs-dataset',
454+
connections: [
455+
{
456+
name: 'main',
457+
query: '*[_type == "document"]',
458+
resourceType: ViewResourceType.Dataset,
459+
resourceId: 'project789.dataset101',
460+
},
461+
],
462+
},
463+
],
464+
}
465+
const client = createViewClient(configWithOverrides)
466+
const result = [{_id: 'obs-dataset-doc', title: 'Observable Dataset Document'}]
467+
468+
// Mock the emulate endpoint (POST request)
469+
nock(`https://${apicdnHost}`)
470+
.post('/v2025-01-01/views/vw-obs-dataset/emulate/emulate?returnQuery=false&perspective=published', {
471+
query: '*[_type == "obs-test"]',
472+
params: {},
473+
// TODO: Add view connections configuration to the body
474+
// This should include the dataset connections from viewOverrides
475+
})
476+
.reply(200, {ms: 120, result})
477+
478+
await new Promise<void>((resolve, reject) => {
479+
client.observable.fetch('vw-obs-dataset', '*[_type == "obs-test"]').subscribe({
480+
next: (res) => {
481+
expect(res).toEqual(result)
482+
},
483+
error: reject,
484+
complete: resolve,
485+
})
486+
})
487+
})
384488
})
385489
})

0 commit comments

Comments
 (0)