Skip to content

Commit fcbbd2b

Browse files
committed
refactor: enforce explicit client config and add tests to achieve coverage
1 parent ee022c6 commit fcbbd2b

File tree

2 files changed

+135
-3
lines changed

2 files changed

+135
-3
lines changed

packages/core/src/client/clientStore.test.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {Subject} from 'rxjs'
33
import {beforeEach, describe, expect, it, vi} from 'vitest'
44

55
import {getAuthMethodState, getTokenState} from '../auth/authStore'
6+
import {datasetSource, mediaLibrarySource} from '../config/sanityConfig'
67
import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
78
import {getClient, getClientState} from './clientStore'
89

@@ -158,4 +159,120 @@ describe('clientStore', () => {
158159
subscription.unsubscribe()
159160
})
160161
})
162+
163+
describe('source handling', () => {
164+
it('should create client when source is provided', () => {
165+
const source = datasetSource('source-project', 'source-dataset')
166+
const client = getClient(instance, {apiVersion: '2024-11-12', source})
167+
168+
expect(vi.mocked(createClient)).toHaveBeenCalledWith(
169+
expect.objectContaining({
170+
apiVersion: '2024-11-12',
171+
source: expect.objectContaining({
172+
__sanity_internal_sourceId: {
173+
projectId: 'source-project',
174+
dataset: 'source-dataset',
175+
},
176+
}),
177+
}),
178+
)
179+
// Client should be projectless - no projectId/dataset in config
180+
expect(client.config()).not.toHaveProperty('projectId')
181+
expect(client.config()).not.toHaveProperty('dataset')
182+
expect(client.config()).toEqual(
183+
expect.objectContaining({
184+
source: expect.objectContaining({
185+
__sanity_internal_sourceId: {
186+
projectId: 'source-project',
187+
dataset: 'source-dataset',
188+
},
189+
}),
190+
}),
191+
)
192+
})
193+
194+
it('should create resource when source has array sourceId and be projectless', () => {
195+
const source = mediaLibrarySource('media-lib-123')
196+
const client = getClient(instance, {apiVersion: '2024-11-12', source})
197+
198+
expect(vi.mocked(createClient)).toHaveBeenCalledWith(
199+
expect.objectContaining({
200+
'~experimental_resource': {type: 'media-library', id: 'media-lib-123'},
201+
'apiVersion': '2024-11-12',
202+
}),
203+
)
204+
// Client should be projectless - no projectId/dataset in config
205+
expect(client.config()).not.toHaveProperty('projectId')
206+
expect(client.config()).not.toHaveProperty('dataset')
207+
expect(client.config()).toEqual(
208+
expect.objectContaining({
209+
'~experimental_resource': {type: 'media-library', id: 'media-lib-123'},
210+
}),
211+
)
212+
})
213+
214+
it('should create projectless client when source is provided, ignoring instance config', () => {
215+
const source = datasetSource('source-project', 'source-dataset')
216+
const client = getClient(instance, {apiVersion: '2024-11-12', source})
217+
218+
// Client should be projectless - source takes precedence, instance config is ignored
219+
expect(client.config()).not.toHaveProperty('projectId')
220+
expect(client.config()).not.toHaveProperty('dataset')
221+
expect(client.config()).toEqual(
222+
expect.objectContaining({
223+
source: expect.objectContaining({
224+
__sanity_internal_sourceId: {
225+
projectId: 'source-project',
226+
dataset: 'source-dataset',
227+
},
228+
}),
229+
}),
230+
)
231+
})
232+
233+
it('should warn when both source and explicit projectId/dataset are provided', () => {
234+
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
235+
const source = datasetSource('source-project', 'source-dataset')
236+
const client = getClient(instance, {
237+
apiVersion: '2024-11-12',
238+
source,
239+
projectId: 'explicit-project',
240+
dataset: 'explicit-dataset',
241+
})
242+
243+
expect(consoleSpy).toHaveBeenCalledWith(
244+
'Both source and explicit projectId/dataset are provided. The source will be used and projectId/dataset will be ignored.',
245+
)
246+
// Client should still be projectless despite explicit projectId/dataset
247+
expect(client.config()).not.toHaveProperty('projectId')
248+
expect(client.config()).not.toHaveProperty('dataset')
249+
consoleSpy.mockRestore()
250+
})
251+
252+
it('should create different clients for different sources', () => {
253+
const source1 = datasetSource('project-1', 'dataset-1')
254+
const source2 = datasetSource('project-2', 'dataset-2')
255+
const source3 = mediaLibrarySource('media-lib-1')
256+
257+
const client1 = getClient(instance, {apiVersion: '2024-11-12', source: source1})
258+
const client2 = getClient(instance, {apiVersion: '2024-11-12', source: source2})
259+
const client3 = getClient(instance, {apiVersion: '2024-11-12', source: source3})
260+
261+
expect(client1).not.toBe(client2)
262+
expect(client2).not.toBe(client3)
263+
expect(client1).not.toBe(client3)
264+
expect(vi.mocked(createClient)).toHaveBeenCalledTimes(3)
265+
})
266+
267+
it('should reuse clients with identical source configurations', () => {
268+
const source = datasetSource('same-project', 'same-dataset')
269+
const options = {apiVersion: '2024-11-12', source}
270+
271+
const client1 = getClient(instance, options)
272+
const client2 = getClient(instance, options)
273+
274+
expect(client1).toBe(client2)
275+
expect(vi.mocked(createClient)).toHaveBeenCalledTimes(1)
276+
})
277+
})
161278
})

packages/core/src/client/clientStore.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ export const getClient = bindActionGlobally(
163163

164164
const tokenFromState = state.get().token
165165
const {clients, authMethod} = state.get()
166+
const hasSource = !!options.source
166167
let sourceId = options.source?.[SOURCE_ID]
167168

168169
let resource
@@ -171,13 +172,13 @@ export const getClient = bindActionGlobally(
171172
sourceId = undefined
172173
}
173174

174-
const projectId = options.projectId ?? instance.config.projectId ?? sourceId?.projectId
175-
const dataset = options.dataset ?? instance.config.dataset ?? sourceId?.dataset
175+
const projectId = options.projectId ?? instance.config.projectId
176+
const dataset = options.dataset ?? instance.config.dataset
176177
const apiHost = options.apiHost ?? instance.config.auth?.apiHost
177178

178179
const effectiveOptions: ClientOptions = {
179180
...DEFAULT_CLIENT_CONFIG,
180-
...((options.scope === 'global' || !projectId) && {useProjectHostname: false}),
181+
...((options.scope === 'global' || !projectId || hasSource) && {useProjectHostname: false}),
181182
token: authMethod === 'cookie' ? undefined : (tokenFromState ?? undefined),
182183
...options,
183184
...(projectId && {projectId}),
@@ -186,6 +187,20 @@ export const getClient = bindActionGlobally(
186187
...(resource && {'~experimental_resource': resource}),
187188
}
188189

190+
// When a source is provided, don't use projectId/dataset - the client should be "projectless"
191+
// The client code itself will ignore the non-source config, so we do this to prevent confusing the user.
192+
// (ref: https://github.com/sanity-io/client/blob/5c23f81f5ab93a53f5b22b39845c867988508d84/src/data/dataMethods.ts#L691)
193+
if (hasSource) {
194+
if (options.projectId || options.dataset) {
195+
// eslint-disable-next-line no-console
196+
console.warn(
197+
'Both source and explicit projectId/dataset are provided. The source will be used and projectId/dataset will be ignored.',
198+
)
199+
}
200+
delete effectiveOptions.projectId
201+
delete effectiveOptions.dataset
202+
}
203+
189204
if (effectiveOptions.token === null || typeof effectiveOptions.token === 'undefined') {
190205
delete effectiveOptions.token
191206
if (authMethod === 'cookie') {

0 commit comments

Comments
 (0)