@sanity/sdk 2.9.0 → 2.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/_chunks-dts/utils.d.ts +105 -51
  2. package/dist/_chunks-es/createGroqSearchFilter.js +131 -54
  3. package/dist/_chunks-es/createGroqSearchFilter.js.map +1 -1
  4. package/dist/_chunks-es/version.js +1 -1
  5. package/dist/_exports/_internal.d.ts +1 -1
  6. package/dist/index.d.ts +2 -2
  7. package/dist/index.js +119 -73
  8. package/dist/index.js.map +1 -1
  9. package/package.json +8 -10
  10. package/src/_exports/index.ts +8 -0
  11. package/src/client/clientStore.test.ts +30 -30
  12. package/src/client/clientStore.ts +47 -47
  13. package/src/comlink/controller/actions/getOrCreateChannel.ts +2 -2
  14. package/src/comlink/node/actions/getOrCreateNode.ts +2 -2
  15. package/src/config/sanityConfig.ts +72 -12
  16. package/src/document/applyDocumentActions.test.ts +7 -7
  17. package/src/document/applyDocumentActions.ts +5 -5
  18. package/src/document/documentStore.test.ts +68 -62
  19. package/src/document/documentStore.ts +36 -36
  20. package/src/document/processActions.ts +2 -2
  21. package/src/document/reducers.ts +4 -4
  22. package/src/document/sharedListener.ts +7 -7
  23. package/src/presence/bifurTransport.test.ts +46 -6
  24. package/src/presence/bifurTransport.ts +13 -1
  25. package/src/presence/presenceStore.test.ts +96 -0
  26. package/src/presence/presenceStore.ts +96 -24
  27. package/src/preview/getPreviewState.ts +1 -1
  28. package/src/preview/previewProjectionUtils.test.ts +4 -4
  29. package/src/preview/previewProjectionUtils.ts +7 -7
  30. package/src/preview/resolvePreview.ts +5 -1
  31. package/src/projection/getProjectionState.ts +4 -4
  32. package/src/projection/projectionStore.test.ts +2 -2
  33. package/src/projection/resolveProjection.ts +2 -2
  34. package/src/projection/subscribeToStateAndFetchBatches.test.ts +1 -1
  35. package/src/projection/subscribeToStateAndFetchBatches.ts +12 -11
  36. package/src/query/queryStore.test.ts +12 -12
  37. package/src/query/queryStore.ts +10 -10
  38. package/src/query/reducers.ts +3 -3
  39. package/src/releases/getPerspectiveState.ts +5 -5
  40. package/src/releases/releasesStore.test.ts +6 -6
  41. package/src/releases/releasesStore.ts +9 -9
  42. package/src/store/createActionBinder.test.ts +31 -31
  43. package/src/store/createActionBinder.ts +43 -38
  44. package/src/store/createSanityInstance.ts +2 -3
  45. package/src/users/reducers.ts +3 -4
  46. package/src/utils/createFetcherStore.ts +6 -4
  47. package/src/utils/isImportError.test.ts +72 -0
  48. package/src/utils/isImportError.ts +34 -0
  49. package/src/utils/object.test.ts +95 -0
  50. package/src/utils/object.ts +142 -0
@@ -7,9 +7,9 @@ import {
7
7
  type SanityDocument,
8
8
  } from '@sanity/types'
9
9
  import {evaluateSync, type ExprNode} from 'groq-js'
10
- import {isEqual} from 'lodash-es'
11
10
 
12
11
  import {isReleasePerspective} from '../releases/utils/isReleasePerspective'
12
+ import {isDeepEqual} from '../utils/object'
13
13
  import {type DocumentAction} from './actions'
14
14
  import {type Grant} from './permissions'
15
15
  import {type DocumentSet, getId, processMutations} from './processMutations'
@@ -568,7 +568,7 @@ export function processActions({
568
568
 
569
569
  // Before proceeding, verify that the working draft is identical to the base draft.
570
570
  // TODO: is it enough just to check for the _rev or nah?
571
- if (!isEqual(workingDraft, baseDraft)) {
571
+ if (!isDeepEqual(workingDraft, baseDraft)) {
572
572
  throw new ActionError({
573
573
  documentId,
574
574
  transactionId,
@@ -1,11 +1,11 @@
1
1
  import {DocumentId, getDraftId, getPublishedId, getVersionId} from '@sanity/id-utils'
2
2
  import {type Mutation, type PatchOperations, type SanityDocumentLike} from '@sanity/types'
3
- import {omit} from 'lodash-es'
4
3
 
5
4
  import {type DocumentHandle} from '../config/sanityConfig'
6
5
  import {isReleasePerspective} from '../releases/utils/isReleasePerspective'
7
6
  import {type StoreContext} from '../store/defineStore'
8
7
  import {insecureRandomId} from '../utils/ids'
8
+ import {omitProperty} from '../utils/object'
9
9
  import {setCleanupTimeout} from '../utils/setCleanupTimeout'
10
10
  import {type DocumentAction} from './actions'
11
11
  import {DOCUMENT_STATE_CLEAR_DELAY} from './documentConstants'
@@ -392,7 +392,7 @@ export function revertOutgoingTransaction(prev: SyncTransactionState): SyncTrans
392
392
  local: documentId in working ? working[documentId] : local,
393
393
  unverifiedRevisions:
394
394
  prev.outgoing && prev.outgoing.transactionId in unverifiedRevisions
395
- ? omit(unverifiedRevisions, prev.outgoing.transactionId)
395
+ ? omitProperty(unverifiedRevisions, prev.outgoing.transactionId)
396
396
  : unverifiedRevisions,
397
397
  }
398
398
  return [documentId, next] as const
@@ -420,7 +420,7 @@ export function applyRemoteDocument(
420
420
  const revisionToVerify = revision ? prevUnverifiedRevisions?.[revision] : undefined
421
421
  let unverifiedRevisions = prevUnverifiedRevisions ?? EMPTY_REVISIONS
422
422
  if (revision && revisionToVerify) {
423
- unverifiedRevisions = omit(prevUnverifiedRevisions, revision)
423
+ unverifiedRevisions = omitProperty(prevUnverifiedRevisions, revision)
424
424
  }
425
425
 
426
426
  // if this remote document is from a `'sync'` event (meaning that the whole
@@ -547,7 +547,7 @@ export function removeSubscriptionIdFromDocument(
547
547
 
548
548
  if (!prevDocState) return prev
549
549
  if (!subscriptions.length) {
550
- return {...prev, documentStates: omit(prev.documentStates, documentId)}
550
+ return {...prev, documentStates: omitProperty(prev.documentStates, documentId)}
551
551
  }
552
552
  return {
553
553
  ...prev,
@@ -14,7 +14,7 @@ import {
14
14
  } from 'rxjs'
15
15
 
16
16
  import {getClientState} from '../client/clientStore'
17
- import {type DocumentSource, isDatasetSource} from '../config/sanityConfig'
17
+ import {type DocumentResource, isDatasetResource} from '../config/sanityConfig'
18
18
  import {type SanityInstance} from '../store/createSanityInstance'
19
19
 
20
20
  const API_VERSION = 'v2025-05-06'
@@ -26,13 +26,13 @@ export interface SharedListener {
26
26
 
27
27
  export function createSharedListener(
28
28
  instance: SanityInstance,
29
- source?: DocumentSource,
29
+ resource?: DocumentResource,
30
30
  ): SharedListener {
31
31
  const dispose$ = new Subject<void>()
32
32
  const events$ = getClientState(instance, {
33
33
  apiVersion: API_VERSION,
34
- // TODO: remove in v3 when we're ready for everything to be queried via source
35
- source: source && !isDatasetSource(source) ? source : undefined,
34
+ // TODO: remove in v3 when we're ready for everything to be queried via resource
35
+ resource: resource && !isDatasetResource(resource) ? resource : undefined,
36
36
  }).observable.pipe(
37
37
  switchMap((client) =>
38
38
  // TODO: it seems like the client.listen method is not emitting disconnected
@@ -68,12 +68,12 @@ export function createSharedListener(
68
68
  }
69
69
  }
70
70
 
71
- export function createFetchDocument(instance: SanityInstance, source?: DocumentSource) {
71
+ export function createFetchDocument(instance: SanityInstance, resource?: DocumentResource) {
72
72
  return function (documentId: string): Observable<SanityDocument | null> {
73
73
  return getClientState(instance, {
74
74
  apiVersion: API_VERSION,
75
- // TODO: remove in v3 when we're ready for everything to be queried via source
76
- source: source && !isDatasetSource(source) ? source : undefined,
75
+ // TODO: remove in v3 when we're ready for everything to be queried via resource
76
+ resource: resource && !isDatasetResource(resource) ? resource : undefined,
77
77
  }).observable.pipe(
78
78
  switchMap((client) => {
79
79
  // creates a observable request to the /doc/{documentId} endpoint for a given document id
@@ -45,16 +45,18 @@ describe('createBifurTransport', () => {
45
45
 
46
46
  beforeEach(() => {
47
47
  vi.useFakeTimers()
48
+ vi.clearAllMocks()
48
49
  mockBifurClient = {
49
50
  listen: vi.fn(() => new Subject<never>()),
50
51
  request: vi.fn(() => of(undefined)),
51
52
  }
52
53
  fromUrlMock.mockReturnValue(mockBifurClient)
53
54
 
55
+ // Default mock is a dataset client using project hostname
54
56
  mockSanityClient = {
55
57
  config: () => ({
56
58
  dataset: 'test-dataset',
57
- url: 'http://localhost:3333',
59
+ url: 'https://test-project.api.sanity.io/v2022-06-30',
58
60
  requestTagPrefix: 'test-tag',
59
61
  }),
60
62
  withConfig: vi.fn().mockReturnThis(),
@@ -63,7 +65,7 @@ describe('createBifurTransport', () => {
63
65
  token$ = new Subject<string | null>()
64
66
  })
65
67
 
66
- it('constructs the bifur client with the correct URL', () => {
68
+ it('constructs the bifur client URL for a dataset resource', () => {
67
69
  createBifurTransport({
68
70
  client: mockSanityClient,
69
71
  token$,
@@ -71,13 +73,51 @@ describe('createBifurTransport', () => {
71
73
  })
72
74
 
73
75
  expect(fromUrlMock).toHaveBeenCalledWith(
74
- 'ws://localhost:3333/socket/test-dataset?tag=test-tag',
75
- {
76
- token$,
77
- },
76
+ 'wss://test-project.api.sanity.io/v2022-06-30/socket/test-dataset?tag=test-tag',
77
+ {token$},
78
+ )
79
+ })
80
+
81
+ it('constructs the bifur client URL for a canvas resource', () => {
82
+ const canvasClient = {
83
+ config: () => ({
84
+ resource: {type: 'canvas', id: 'canvas-123'},
85
+ url: 'https://api.sanity.io/v2022-06-30',
86
+ requestTagPrefix: 'test-tag',
87
+ }),
88
+ withConfig: vi.fn().mockReturnThis(),
89
+ } as unknown as SanityClient
90
+
91
+ createBifurTransport({
92
+ client: canvasClient,
93
+ token$,
94
+ sessionId: 'session-id-123',
95
+ })
96
+
97
+ expect(fromUrlMock).toHaveBeenCalledWith(
98
+ 'wss://api.sanity.io/v2022-06-30/socket/canvases/canvas-123?tag=test-tag',
99
+ {token$},
78
100
  )
79
101
  })
80
102
 
103
+ it('throws when no canvas resource or dataset is configured', () => {
104
+ const invalidClient = {
105
+ config: () => ({
106
+ url: 'https://api.sanity.io/v2022-06-30',
107
+ requestTagPrefix: 'test-tag',
108
+ }),
109
+ withConfig: vi.fn().mockReturnThis(),
110
+ } as unknown as SanityClient
111
+
112
+ expect(() =>
113
+ createBifurTransport({
114
+ client: invalidClient,
115
+ token$,
116
+ sessionId: 'session-id-123',
117
+ }),
118
+ ).toThrow('Unable to determine presence URL')
119
+ })
120
+
81
121
  it('handles incoming rollCall events', () => {
82
122
  const incomingBifurEvents$ = new Subject<IncomingBifurEvent>()
83
123
  mockBifurClient.listen.mockReturnValue(incomingBifurEvents$)
@@ -37,11 +37,23 @@ type IncomingBifurEvent = RollCallEvent | BifurStateMessage | BifurDisconnectMes
37
37
  function getBifurClient(client: SanityClient, token$: Observable<string | null>): BifurClient {
38
38
  const bifurVersionedClient = client.withConfig({apiVersion: '2022-06-30'})
39
39
  const {
40
+ resource,
40
41
  dataset,
41
42
  url: baseUrl,
42
43
  requestTagPrefix = 'sanity.sdk.presence',
43
44
  } = bifurVersionedClient.config()
44
- const url = `${baseUrl.replace(/\/+$/, '')}/socket/${dataset}`.replace(/^http/, 'ws')
45
+
46
+ let resourcePath: string
47
+ if (resource?.type === 'canvas') {
48
+ resourcePath = `canvases/${resource.id}`
49
+ } else if (dataset) {
50
+ // Dataset clients use project hostname — dataset name alone is the socket path
51
+ resourcePath = dataset
52
+ } else {
53
+ throw new Error(`Unable to determine presence URL: no canvas resource or dataset configured`)
54
+ }
55
+
56
+ const url = `${baseUrl}/socket/${resourcePath}`.replace(/^http/, 'ws')
45
57
 
46
58
  const urlWithTag = `${url}?tag=${requestTagPrefix}`
47
59
 
@@ -48,6 +48,9 @@ describe('presenceStore', () => {
48
48
 
49
49
  mockClient = {
50
50
  withConfig: vi.fn().mockReturnThis(),
51
+ observable: {
52
+ request: vi.fn(() => of({organizationId: 'test-org-id'})),
53
+ },
51
54
  } as unknown as SanityClient
52
55
 
53
56
  mockTokenState = new Subject<string | null>()
@@ -243,5 +246,98 @@ describe('presenceStore', () => {
243
246
 
244
247
  unsubscribe()
245
248
  })
249
+
250
+ it('should throw an error when initialized with a media library resource', () => {
251
+ const mediaLibraryResource = {mediaLibraryId: 'ml123'}
252
+
253
+ expect(() => {
254
+ getPresence(instance, {resource: mediaLibraryResource})
255
+ }).toThrow('Presence is not supported for media library resources.')
256
+ })
257
+
258
+ it('should work with a dataset resource', () => {
259
+ const datasetResource = {projectId: 'test-project', dataset: 'test-dataset'}
260
+
261
+ expect(() => {
262
+ getPresence(instance, {resource: datasetResource})
263
+ }).not.toThrow()
264
+ })
265
+
266
+ it('should work with a canvas resource', () => {
267
+ const canvasResource = {canvasId: 'canvas123'}
268
+
269
+ expect(() => {
270
+ getPresence(instance, {resource: canvasResource})
271
+ }).not.toThrow()
272
+ })
273
+
274
+ it('creates a project-hostname client for dataset resources', () => {
275
+ getPresence(instance, {resource: {projectId: 'my-project', dataset: 'my-dataset'}})
276
+
277
+ expect(getClient).toHaveBeenCalledWith(instance, {
278
+ apiVersion: '2026-03-30',
279
+ projectId: 'my-project',
280
+ dataset: 'my-dataset',
281
+ useProjectHostname: true,
282
+ })
283
+ })
284
+
285
+ it('creates a resource client for canvas resources', () => {
286
+ const canvasResource = {canvasId: 'canvas123'}
287
+ getPresence(instance, {resource: canvasResource})
288
+
289
+ expect(getClient).toHaveBeenCalledWith(instance, {
290
+ apiVersion: '2026-03-30',
291
+ resource: canvasResource,
292
+ })
293
+ })
294
+
295
+ it('fetches organizationId from canvas endpoint for canvas resources', () => {
296
+ const canvasResource = {canvasId: 'canvas123'}
297
+ getPresence(instance, {resource: canvasResource})
298
+
299
+ expect(mockClient.observable.request).toHaveBeenCalledWith({
300
+ uri: '/canvases/canvas123',
301
+ tag: 'canvases.get',
302
+ })
303
+ })
304
+
305
+ it('does not fetch organizationId for dataset resources', () => {
306
+ getPresence(instance, {resource: {projectId: 'my-project', dataset: 'my-dataset'}})
307
+
308
+ expect(mockClient.observable.request).not.toHaveBeenCalled()
309
+ })
310
+
311
+ it('fetches user data for canvas users', async () => {
312
+ const source = getPresence(instance, {resource: {canvasId: 'canvas123'}})
313
+ const unsubscribe = source.subscribe(() => {})
314
+
315
+ await firstValueFrom(of(null).pipe(delay(10)))
316
+
317
+ mockIncomingEvents.next({
318
+ type: 'state',
319
+ userId: 'user-1',
320
+ sessionId: 'other-session',
321
+ timestamp: '2023-01-01T12:00:00Z',
322
+ locations: [
323
+ {
324
+ type: 'document',
325
+ documentId: 'doc-1',
326
+ path: ['title'],
327
+ lastActiveAt: '2023-01-01T12:00:00Z',
328
+ },
329
+ ],
330
+ })
331
+
332
+ await firstValueFrom(of(null).pipe(delay(50)))
333
+
334
+ expect(getUserState).toHaveBeenCalledWith(instance, {
335
+ userId: 'user-1',
336
+ resourceType: 'organization',
337
+ organizationId: 'test-org-id',
338
+ })
339
+
340
+ unsubscribe()
341
+ })
246
342
  })
247
343
  })
@@ -1,20 +1,45 @@
1
- import {type SanityClient} from '@sanity/client'
2
1
  import {createSelector} from 'reselect'
3
- import {combineLatest, distinctUntilChanged, filter, map, of, Subscription, switchMap} from 'rxjs'
2
+ import {
3
+ catchError,
4
+ combineLatest,
5
+ distinctUntilChanged,
6
+ EMPTY,
7
+ filter,
8
+ first,
9
+ map,
10
+ type Observable,
11
+ of,
12
+ Subscription,
13
+ switchMap,
14
+ } from 'rxjs'
4
15
 
5
16
  import {getTokenState} from '../auth/authStore'
6
17
  import {getClient} from '../client/clientStore'
7
- import {bindActionByDataset, type BoundDatasetKey} from '../store/createActionBinder'
8
- import {createStateSourceAction, type SelectorContext} from '../store/createStateSourceAction'
18
+ import {
19
+ type DocumentResource,
20
+ isCanvasResource,
21
+ isDatasetResource,
22
+ isMediaLibraryResource,
23
+ } from '../config/sanityConfig'
24
+ import {bindActionByResource, type BoundResourceKey} from '../store/createActionBinder'
25
+ import {type SanityInstance} from '../store/createSanityInstance'
26
+ import {
27
+ createStateSourceAction,
28
+ type SelectorContext,
29
+ type StateSource,
30
+ } from '../store/createStateSourceAction'
9
31
  import {defineStore, type StoreContext} from '../store/defineStore'
10
32
  import {type SanityUser} from '../users/types'
11
33
  import {getUserState} from '../users/usersStore'
12
34
  import {createBifurTransport} from './bifurTransport'
13
35
  import {type PresenceLocation, type TransportEvent, type UserPresence} from './types'
14
36
 
37
+ const PRESENCE_API_VERSION = '2026-03-30'
38
+
15
39
  type PresenceStoreState = {
16
40
  locations: Map<string, {userId: string; locations: PresenceLocation[]}>
17
41
  users: Record<string, SanityUser | undefined>
42
+ organizationId?: string
18
43
  }
19
44
 
20
45
  const getInitialState = (): PresenceStoreState => ({
@@ -23,27 +48,40 @@ const getInitialState = (): PresenceStoreState => ({
23
48
  })
24
49
 
25
50
  /** @public */
26
- export const presenceStore = defineStore<PresenceStoreState, BoundDatasetKey>({
51
+ export const presenceStore = defineStore<PresenceStoreState, BoundResourceKey>({
27
52
  name: 'presence',
28
53
  getInitialState,
29
- initialize: (context: StoreContext<PresenceStoreState, BoundDatasetKey>) => {
54
+ initialize: (context: StoreContext<PresenceStoreState, BoundResourceKey>) => {
30
55
  const {
31
56
  instance,
32
57
  state,
33
- key: {projectId, dataset},
58
+ key: {resource},
34
59
  } = context
60
+
61
+ if (isMediaLibraryResource(resource)) {
62
+ throw new Error('Presence is not supported for media library resources.')
63
+ }
64
+
35
65
  const sessionId = crypto.randomUUID()
36
66
 
37
- const client = getClient(instance, {
38
- apiVersion: '2022-06-30',
39
- projectId,
40
- dataset,
41
- })
67
+ // Dataset resources must use the project hostname so the socket URL is project-specific.
68
+ // Canvas resources use the global API endpoint via the resource config.
69
+ const client = isDatasetResource(resource)
70
+ ? getClient(instance, {
71
+ apiVersion: PRESENCE_API_VERSION,
72
+ projectId: resource.projectId,
73
+ dataset: resource.dataset,
74
+ useProjectHostname: true,
75
+ })
76
+ : getClient(instance, {
77
+ apiVersion: PRESENCE_API_VERSION,
78
+ resource,
79
+ })
42
80
 
43
81
  const token$ = getTokenState(instance).observable.pipe(distinctUntilChanged())
44
82
 
45
83
  const [incomingEvents$, dispatch] = createBifurTransport({
46
- client: client as SanityClient,
84
+ client,
47
85
  token$,
48
86
  sessionId,
49
87
  })
@@ -81,6 +119,22 @@ export const presenceStore = defineStore<PresenceStoreState, BoundDatasetKey>({
81
119
 
82
120
  dispatch({type: 'rollCall'}).subscribe()
83
121
 
122
+ // Canvas resources need the organizationId to resolve users — fetch it once from the canvas endpoint
123
+ if (isCanvasResource(resource)) {
124
+ const globalClient = getClient(instance, {apiVersion: PRESENCE_API_VERSION})
125
+ subscription.add(
126
+ globalClient.observable
127
+ .request<{organizationId: string}>({
128
+ uri: `/canvases/${resource.canvasId}`,
129
+ tag: 'canvases.get',
130
+ })
131
+ .pipe(catchError(() => EMPTY))
132
+ .subscribe(({organizationId}) => {
133
+ state.set('presence/organizationId', (prev) => ({...prev, organizationId}))
134
+ }),
135
+ )
136
+ }
137
+
84
138
  return () => {
85
139
  dispatch({type: 'disconnect'}).subscribe()
86
140
  subscription.unsubscribe()
@@ -114,13 +168,13 @@ const selectPresence = createSelector(
114
168
  },
115
169
  )
116
170
 
117
- /** @public */
118
- export const getPresence = bindActionByDataset(
171
+ const _getPresence = bindActionByResource(
119
172
  presenceStore,
120
173
  createStateSourceAction({
121
- selector: (context: SelectorContext<PresenceStoreState>, _?): UserPresence[] =>
174
+ selector: (context: SelectorContext<PresenceStoreState>): UserPresence[] =>
122
175
  selectPresence(context.state),
123
- onSubscribe: (context: StoreContext<PresenceStoreState, BoundDatasetKey>, _?) => {
176
+ onSubscribe: (context: StoreContext<PresenceStoreState, BoundResourceKey>) => {
177
+ const resource = context.key.resource
124
178
  const userIds$ = context.state.observable.pipe(
125
179
  map((state) =>
126
180
  Array.from(state.locations.values())
@@ -130,26 +184,34 @@ export const getPresence = bindActionByDataset(
130
184
  distinctUntilChanged((a, b) => a.length === b.length && a.every((v, i) => v === b[i])),
131
185
  )
132
186
 
133
- const subscription = userIds$
187
+ // For canvas resources, wait for organizationId to be fetched and stored in state.
188
+ // For dataset resources, emit undefined immediately so the stream isn't blocked.
189
+ const organizationId$: Observable<string | undefined> = isCanvasResource(resource)
190
+ ? context.state.observable.pipe(
191
+ map((s) => s.organizationId),
192
+ filter((id): id is string => id !== undefined),
193
+ first(),
194
+ )
195
+ : of(undefined)
196
+
197
+ const subscription = combineLatest([userIds$, organizationId$])
134
198
  .pipe(
135
- switchMap((userIds) => {
199
+ switchMap(([userIds, organizationId]) => {
136
200
  if (userIds.length === 0) {
137
201
  return of([])
138
202
  }
139
203
  const userObservables = userIds.map((userId) =>
140
204
  getUserState(context.instance, {
141
205
  userId,
142
- resourceType: 'project',
143
- projectId: context.key.projectId,
206
+ ...(isDatasetResource(resource)
207
+ ? {resourceType: 'project', projectId: resource.projectId}
208
+ : {resourceType: 'organization', organizationId}),
144
209
  }).pipe(filter((v): v is NonNullable<typeof v> => !!v)),
145
210
  )
146
211
  return combineLatest(userObservables)
147
212
  }),
148
213
  )
149
214
  .subscribe((users) => {
150
- if (!users) {
151
- return
152
- }
153
215
  context.state.set('presence/users', (prevState) => ({
154
216
  ...prevState,
155
217
  users: {
@@ -167,3 +229,13 @@ export const getPresence = bindActionByDataset(
167
229
  },
168
230
  }),
169
231
  )
232
+
233
+ /** @beta */
234
+ export function getPresence(
235
+ instance: SanityInstance,
236
+ params?: {resource?: DocumentResource},
237
+ ): StateSource<UserPresence[]> {
238
+ // bit of a hack to support the old bound action by dataset
239
+ // in reality, this will always be passed a resource
240
+ return _getPresence(instance, params ?? {})
241
+ }
@@ -52,7 +52,7 @@ export function getPreviewState(
52
52
  return {data: null, isPending: current?.isPending ?? false}
53
53
  }
54
54
 
55
- const previewValue = transformProjectionToPreview(instance, current.data, options.source)
55
+ const previewValue = transformProjectionToPreview(instance, current.data, options.resource)
56
56
 
57
57
  return {
58
58
  data: previewValue,
@@ -156,7 +156,7 @@ describe('transformProjectionToPreview', () => {
156
156
  })
157
157
  })
158
158
 
159
- it('calls getClient with the provided source', async () => {
159
+ it('calls getClient with the provided resource', async () => {
160
160
  const projectionResult: PreviewQueryResult = {
161
161
  _id: 'doc1',
162
162
  _type: 'article',
@@ -166,14 +166,14 @@ describe('transformProjectionToPreview', () => {
166
166
  media: null,
167
167
  }
168
168
 
169
- const source = {mediaLibraryId: 'test-library'}
169
+ const resource = {mediaLibraryId: 'test-library'}
170
170
 
171
- transformProjectionToPreview(instance, projectionResult, source)
171
+ transformProjectionToPreview(instance, projectionResult, resource)
172
172
 
173
173
  const {getClient} = await import('../client/clientStore')
174
174
  expect(getClient).toHaveBeenCalledWith(instance, {
175
175
  apiVersion: 'v2025-05-06',
176
- source,
176
+ resource,
177
177
  })
178
178
  })
179
179
  })
@@ -1,10 +1,10 @@
1
1
  import {type SanityClient} from '@sanity/client'
2
2
  import {createImageUrlBuilder} from '@sanity/image-url'
3
- import {isObject} from 'lodash-es'
4
3
 
5
4
  import {getClient} from '../client/clientStore'
6
- import {type DocumentSource, isDatasetSource} from '../config/sanityConfig'
5
+ import {type DocumentResource, isDatasetResource} from '../config/sanityConfig'
7
6
  import {type SanityInstance} from '../store/createSanityInstance'
7
+ import {isObject} from '../utils/object'
8
8
  import {SUBTITLE_CANDIDATES, TITLE_CANDIDATES} from './previewConstants'
9
9
  import {type PreviewQueryResult, type PreviewValue} from './types'
10
10
 
@@ -66,22 +66,22 @@ function findFirstDefined(
66
66
  *
67
67
  * @param projectionResult - The raw projection result from GROQ
68
68
  * @param instance - The Sanity instance to use for client configuration
69
- * @param source - Data source for the preview
69
+ * @param resource - Data resource for the preview
70
70
  * @internal
71
71
  */
72
72
  export function transformProjectionToPreview(
73
73
  instance: SanityInstance,
74
74
  projectionResult: PreviewQueryResult,
75
- source?: DocumentSource,
75
+ resource?: DocumentResource,
76
76
  ): PreviewValue {
77
77
  const title = findFirstDefined(TITLE_CANDIDATES, projectionResult.titleCandidates)
78
78
  const subtitle = findFirstDefined(SUBTITLE_CANDIDATES, projectionResult.subtitleCandidates, title)
79
79
 
80
- // Get a client for the source (if provided) or use the instance config
80
+ // Get a client for the resource (if provided) or use the instance config
81
81
  const client = getClient(instance, {
82
82
  apiVersion: API_VERSION,
83
- // TODO: remove in v3 when we're ready for everything to be queried via source
84
- source: source && !isDatasetSource(source) ? source : undefined,
83
+ // TODO: remove in v3 when we're ready for everything to be queried via resource
84
+ resource: resource && !isDatasetResource(resource) ? resource : undefined,
85
85
  })
86
86
 
87
87
  return {
@@ -30,7 +30,11 @@ export async function resolvePreview(
30
30
  }
31
31
 
32
32
  // Transform to preview format
33
- const previewValue = transformProjectionToPreview(instance, projectionResult.data, options.source)
33
+ const previewValue = transformProjectionToPreview(
34
+ instance,
35
+ projectionResult.data,
36
+ options.resource,
37
+ )
34
38
 
35
39
  return {
36
40
  data: previewValue,
@@ -1,9 +1,8 @@
1
1
  import {DocumentId, getPublishedId} from '@sanity/id-utils'
2
2
  import {type SanityProjectionResult} from 'groq'
3
- import {omit} from 'lodash-es'
4
3
 
5
4
  import {type DocumentHandle} from '../config/sanityConfig'
6
- import {bindActionBySourceAndPerspective} from '../store/createActionBinder'
5
+ import {bindActionByResourceAndPerspective} from '../store/createActionBinder'
7
6
  import {type SanityInstance} from '../store/createSanityInstance'
8
7
  import {
9
8
  createStateSourceAction,
@@ -12,6 +11,7 @@ import {
12
11
  } from '../store/createStateSourceAction'
13
12
  import {hashString} from '../utils/hashString'
14
13
  import {insecureRandomId} from '../utils/ids'
14
+ import {omitProperty} from '../utils/object'
15
15
  import {setCleanupTimeout} from '../utils/setCleanupTimeout'
16
16
  import {projectionStore} from './projectionStore'
17
17
  import {type ProjectionStoreState, type ProjectionValuePending} from './types'
@@ -72,7 +72,7 @@ export function getProjectionState(
72
72
  /**
73
73
  * @beta
74
74
  */
75
- export const _getProjectionState = bindActionBySourceAndPerspective(
75
+ export const _getProjectionState = bindActionByResourceAndPerspective(
76
76
  projectionStore,
77
77
  createStateSourceAction({
78
78
  selector: (
@@ -113,7 +113,7 @@ export const _getProjectionState = bindActionBySourceAndPerspective(
113
113
  return () => {
114
114
  setCleanupTimeout(() => {
115
115
  state.set('removeSubscription', (prev): Partial<ProjectionStoreState> => {
116
- const documentSubscriptionsForHash = omit(
116
+ const documentSubscriptionsForHash = omitProperty(
117
117
  prev.subscriptions[documentId]?.[projectionHash],
118
118
  subscriptionId,
119
119
  )
@@ -30,7 +30,7 @@ describe('projectionStore', () => {
30
30
  instance,
31
31
  {
32
32
  name: 'p.d',
33
- source: {projectId: 'p', dataset: 'd'},
33
+ resource: {projectId: 'p', dataset: 'd'},
34
34
  perspective: 'drafts',
35
35
  },
36
36
  projectionStore,
@@ -42,7 +42,7 @@ describe('projectionStore', () => {
42
42
  state,
43
43
  key: {
44
44
  name: 'p.d',
45
- source: {projectId: 'p', dataset: 'd'},
45
+ resource: {projectId: 'p', dataset: 'd'},
46
46
  perspective: 'drafts',
47
47
  },
48
48
  })