@sanity/sdk 2.8.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.
- package/dist/_chunks-dts/utils.d.ts +2450 -0
- package/dist/_chunks-es/_internal.js +129 -0
- package/dist/_chunks-es/_internal.js.map +1 -0
- package/dist/_chunks-es/createGroqSearchFilter.js +1537 -0
- package/dist/_chunks-es/createGroqSearchFilter.js.map +1 -0
- package/dist/_chunks-es/telemetryManager.js +87 -0
- package/dist/_chunks-es/telemetryManager.js.map +1 -0
- package/dist/_chunks-es/version.js +7 -0
- package/dist/_chunks-es/version.js.map +1 -0
- package/dist/_exports/_internal.d.ts +64 -0
- package/dist/_exports/_internal.js +20 -0
- package/dist/_exports/_internal.js.map +1 -0
- package/dist/index.d.ts +2 -2343
- package/dist/index.js +465 -1813
- package/dist/index.js.map +1 -1
- package/package.json +17 -12
- package/src/_exports/_internal.ts +14 -0
- package/src/_exports/index.ts +18 -1
- package/src/auth/authStore.test.ts +150 -1
- package/src/auth/authStore.ts +11 -11
- package/src/auth/dashboardAuth.ts +2 -2
- package/src/auth/handleAuthCallback.ts +9 -3
- package/src/auth/logout.test.ts +1 -1
- package/src/auth/logout.ts +1 -1
- package/src/auth/refreshStampedToken.test.ts +118 -1
- package/src/auth/refreshStampedToken.ts +3 -2
- package/src/auth/standaloneAuth.ts +9 -3
- package/src/auth/studioAuth.ts +34 -7
- package/src/auth/studioModeAuth.ts +2 -1
- package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +10 -2
- package/src/auth/subscribeToStateAndFetchCurrentUser.ts +5 -1
- package/src/auth/subscribeToStorageEventsAndSetToken.ts +2 -2
- package/src/auth/utils.ts +33 -0
- package/src/client/clientStore.test.ts +44 -30
- package/src/client/clientStore.ts +49 -48
- package/src/comlink/controller/actions/getOrCreateChannel.ts +2 -2
- package/src/comlink/node/actions/getOrCreateNode.ts +2 -2
- package/src/comlink/node/getNodeState.ts +2 -1
- package/src/config/sanityConfig.ts +78 -12
- package/src/document/actions.ts +18 -11
- package/src/document/applyDocumentActions.test.ts +7 -6
- package/src/document/applyDocumentActions.ts +10 -4
- package/src/document/documentStore.test.ts +542 -188
- package/src/document/documentStore.ts +142 -76
- package/src/document/events.ts +7 -2
- package/src/document/permissions.test.ts +18 -16
- package/src/document/permissions.ts +35 -11
- package/src/document/processActions.test.ts +359 -32
- package/src/document/processActions.ts +106 -78
- package/src/document/reducers.test.ts +117 -29
- package/src/document/reducers.ts +47 -40
- package/src/document/sharedListener.ts +16 -6
- package/src/document/util.ts +14 -0
- package/src/favorites/favorites.test.ts +9 -2
- package/src/presence/bifurTransport.test.ts +46 -6
- package/src/presence/bifurTransport.ts +19 -2
- package/src/presence/presenceStore.test.ts +96 -0
- package/src/presence/presenceStore.ts +96 -24
- package/src/preview/getPreviewState.test.ts +115 -98
- package/src/preview/getPreviewState.ts +38 -60
- package/src/preview/previewProjectionUtils.test.ts +179 -0
- package/src/preview/previewProjectionUtils.ts +93 -0
- package/src/preview/resolvePreview.test.ts +42 -25
- package/src/preview/resolvePreview.ts +33 -10
- package/src/preview/{previewStore.ts → types.ts} +8 -17
- package/src/projection/getProjectionState.test.ts +16 -16
- package/src/projection/getProjectionState.ts +6 -5
- package/src/projection/projectionQuery.ts +2 -3
- package/src/projection/projectionStore.test.ts +2 -2
- package/src/projection/resolveProjection.ts +2 -2
- package/src/projection/subscribeToStateAndFetchBatches.test.ts +1 -1
- package/src/projection/subscribeToStateAndFetchBatches.ts +12 -11
- package/src/projection/types.ts +1 -1
- package/src/query/queryStore.test.ts +12 -12
- package/src/query/queryStore.ts +12 -11
- package/src/query/reducers.ts +3 -3
- package/src/releases/getPerspectiveState.ts +7 -6
- package/src/releases/releasesStore.test.ts +20 -5
- package/src/releases/releasesStore.ts +20 -8
- package/src/store/createActionBinder.test.ts +31 -31
- package/src/store/createActionBinder.ts +43 -38
- package/src/store/createSanityInstance.ts +2 -3
- package/src/store/createStateSourceAction.test.ts +62 -0
- package/src/store/createStateSourceAction.ts +34 -39
- package/src/telemetry/__telemetry__/sdk.telemetry.ts +42 -0
- package/src/telemetry/devMode.test.ts +52 -0
- package/src/telemetry/devMode.ts +40 -0
- package/src/telemetry/initTelemetry.test.ts +225 -0
- package/src/telemetry/initTelemetry.ts +205 -0
- package/src/telemetry/telemetryManager.test.ts +263 -0
- package/src/telemetry/telemetryManager.ts +187 -0
- package/src/users/reducers.ts +3 -4
- package/src/users/usersStore.test.ts +1 -0
- package/src/users/usersStore.ts +5 -1
- package/src/utils/createFetcherStore.test.ts +6 -4
- package/src/utils/createFetcherStore.ts +8 -5
- package/src/utils/getStagingApiHost.test.ts +21 -0
- package/src/utils/getStagingApiHost.ts +14 -0
- package/src/utils/ids.test.ts +1 -29
- package/src/utils/ids.ts +0 -10
- package/src/utils/isImportError.test.ts +72 -0
- package/src/utils/isImportError.ts +34 -0
- package/src/utils/object.test.ts +95 -0
- package/src/utils/object.ts +142 -0
- package/src/utils/setCleanupTimeout.ts +24 -0
- package/src/preview/previewQuery.test.ts +0 -236
- package/src/preview/previewQuery.ts +0 -153
- package/src/preview/previewStore.test.ts +0 -36
- package/src/preview/subscribeToStateAndFetchBatches.test.ts +0 -221
- package/src/preview/subscribeToStateAndFetchBatches.ts +0 -112
- package/src/preview/util.ts +0 -13
package/src/auth/studioAuth.ts
CHANGED
|
@@ -9,7 +9,7 @@ import {refreshStampedToken} from './refreshStampedToken'
|
|
|
9
9
|
import {checkForCookieAuth, getStudioTokenFromLocalStorage} from './studioModeAuth'
|
|
10
10
|
import {subscribeToStateAndFetchCurrentUser} from './subscribeToStateAndFetchCurrentUser'
|
|
11
11
|
import {subscribeToStorageEventsAndSetToken} from './subscribeToStorageEventsAndSetToken'
|
|
12
|
-
import {getDefaultStorage} from './utils'
|
|
12
|
+
import {createLoggedInAuthState, getDefaultStorage} from './utils'
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Resolves the initial auth state for Studio mode.
|
|
@@ -55,7 +55,7 @@ export function getStudioInitialState(options: AuthStrategyOptions): AuthStrateg
|
|
|
55
55
|
|
|
56
56
|
if (providedToken) {
|
|
57
57
|
return {
|
|
58
|
-
authState:
|
|
58
|
+
authState: createLoggedInAuthState(providedToken, null),
|
|
59
59
|
storageKey: studioStorageKey,
|
|
60
60
|
storageArea,
|
|
61
61
|
authMethod,
|
|
@@ -65,7 +65,7 @@ export function getStudioInitialState(options: AuthStrategyOptions): AuthStrateg
|
|
|
65
65
|
|
|
66
66
|
if (token) {
|
|
67
67
|
return {
|
|
68
|
-
authState:
|
|
68
|
+
authState: createLoggedInAuthState(token, null),
|
|
69
69
|
storageKey: studioStorageKey,
|
|
70
70
|
storageArea,
|
|
71
71
|
authMethod: 'localstorage',
|
|
@@ -115,14 +115,29 @@ export function initializeStudioAuth(
|
|
|
115
115
|
|
|
116
116
|
/**
|
|
117
117
|
* Subscribe to a reactive token source from the Studio workspace.
|
|
118
|
-
*
|
|
119
|
-
*
|
|
118
|
+
*
|
|
119
|
+
* When the token source emits a non-null token, the SDK uses it directly.
|
|
120
|
+
* When it emits `null`, the behavior depends on the `authenticated` flag
|
|
121
|
+
* from the Studio's workspace config:
|
|
122
|
+
*
|
|
123
|
+
* - `authenticated: true` — the Studio has already verified the user is
|
|
124
|
+
* logged in (e.g. via cookie auth). The SDK treats the null token as
|
|
125
|
+
* cookie-based auth and stays in the LOGGED_IN state.
|
|
126
|
+
*
|
|
127
|
+
* - `authenticated` absent/false — the user is genuinely not authenticated;
|
|
128
|
+
* transition to LOGGED_OUT.
|
|
129
|
+
*
|
|
130
|
+
* No async cookie probing is needed here because this code path only runs
|
|
131
|
+
* when a Studio provides SDKStudioContext, and the Studio's Workspace type
|
|
132
|
+
* always includes `authenticated`. The async `checkForCookieAuth` fallback
|
|
133
|
+
* remains in `initializeWithFallback` for the non-Studio path.
|
|
120
134
|
*/
|
|
121
135
|
function initializeWithTokenSource(
|
|
122
136
|
context: StoreContext<AuthStoreState>,
|
|
123
137
|
tokenSource: TokenSource,
|
|
124
138
|
): {dispose: () => void; tokenRefresherStarted: boolean} {
|
|
125
139
|
const subscriptions: Subscription[] = []
|
|
140
|
+
const studioAuthenticated = context.instance.config.studio?.authenticated === true
|
|
126
141
|
|
|
127
142
|
// Subscribe to the current user fetcher — runs whenever auth state changes
|
|
128
143
|
subscriptions.push(subscribeToStateAndFetchCurrentUser(context, {useProjectHostname: true}))
|
|
@@ -132,11 +147,23 @@ function initializeWithTokenSource(
|
|
|
132
147
|
next: (token) => {
|
|
133
148
|
const {state} = context
|
|
134
149
|
if (token) {
|
|
150
|
+
// Studio provided a real token — use it directly
|
|
135
151
|
state.set('studioTokenSource', (prev) => ({
|
|
136
152
|
options: {...prev.options, authMethod: undefined},
|
|
137
|
-
authState:
|
|
153
|
+
authState: createLoggedInAuthState(token, null),
|
|
154
|
+
}))
|
|
155
|
+
} else if (studioAuthenticated) {
|
|
156
|
+
// The Studio says the user is authenticated — null token means
|
|
157
|
+
// cookie-based auth is in use. Stay logged in with cookie method.
|
|
158
|
+
state.set('studioTokenSourceCookieAuth', (prev) => ({
|
|
159
|
+
options: {...prev.options, authMethod: 'cookie'},
|
|
160
|
+
authState:
|
|
161
|
+
prev.authState.type === AuthStateType.LOGGED_IN
|
|
162
|
+
? prev.authState
|
|
163
|
+
: createLoggedInAuthState('', null),
|
|
138
164
|
}))
|
|
139
165
|
} else {
|
|
166
|
+
// No token and Studio doesn't confirm authentication — logged out
|
|
140
167
|
state.set('studioTokenSourceLoggedOut', (prev) => ({
|
|
141
168
|
options: {...prev.options, authMethod: undefined},
|
|
142
169
|
authState: {type: AuthStateType.LOGGED_OUT, isDestroyingSession: false},
|
|
@@ -193,7 +220,7 @@ function initializeWithFallback(
|
|
|
193
220
|
authState:
|
|
194
221
|
prev.authState.type === AuthStateType.LOGGED_IN
|
|
195
222
|
? prev.authState
|
|
196
|
-
:
|
|
223
|
+
: createLoggedInAuthState('', null),
|
|
197
224
|
}))
|
|
198
225
|
})
|
|
199
226
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {type ClientConfig, type SanityClient} from '@sanity/client'
|
|
2
2
|
|
|
3
|
+
import {REQUEST_TAG_PREFIX} from './authConstants'
|
|
3
4
|
import {getTokenFromStorage} from './utils'
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -25,7 +26,7 @@ export async function checkForCookieAuth(
|
|
|
25
26
|
const client = clientFactory({
|
|
26
27
|
projectId,
|
|
27
28
|
useCdn: false,
|
|
28
|
-
requestTagPrefix:
|
|
29
|
+
requestTagPrefix: REQUEST_TAG_PREFIX,
|
|
29
30
|
timeout: COOKIE_AUTH_TIMEOUT_MS,
|
|
30
31
|
})
|
|
31
32
|
const user = await client.request({
|
|
@@ -40,7 +40,11 @@ describe('subscribeToStateAndFetchCurrentUser', () => {
|
|
|
40
40
|
useProjectHostname: false,
|
|
41
41
|
useCdn: false,
|
|
42
42
|
})
|
|
43
|
-
expect(mockRequest).toHaveBeenCalledWith({
|
|
43
|
+
expect(mockRequest).toHaveBeenCalledWith({
|
|
44
|
+
method: 'GET',
|
|
45
|
+
uri: '/users/me',
|
|
46
|
+
tag: 'users.get-current',
|
|
47
|
+
})
|
|
44
48
|
|
|
45
49
|
subscription.unsubscribe()
|
|
46
50
|
})
|
|
@@ -76,7 +80,11 @@ describe('subscribeToStateAndFetchCurrentUser', () => {
|
|
|
76
80
|
useProjectHostname: false,
|
|
77
81
|
useCdn: false,
|
|
78
82
|
})
|
|
79
|
-
expect(mockRequest).toHaveBeenCalledWith({
|
|
83
|
+
expect(mockRequest).toHaveBeenCalledWith({
|
|
84
|
+
method: 'GET',
|
|
85
|
+
uri: '/users/me',
|
|
86
|
+
tag: 'users.get-current',
|
|
87
|
+
})
|
|
80
88
|
|
|
81
89
|
subscription.unsubscribe()
|
|
82
90
|
})
|
|
@@ -60,7 +60,11 @@ export const subscribeToStateAndFetchCurrentUser = (
|
|
|
60
60
|
}),
|
|
61
61
|
),
|
|
62
62
|
switchMap((client) =>
|
|
63
|
-
client.observable.request<CurrentUser>({
|
|
63
|
+
client.observable.request<CurrentUser>({
|
|
64
|
+
uri: '/users/me',
|
|
65
|
+
method: 'GET',
|
|
66
|
+
tag: 'users.get-current',
|
|
67
|
+
}),
|
|
64
68
|
),
|
|
65
69
|
)
|
|
66
70
|
|
|
@@ -3,7 +3,7 @@ import {defer, distinctUntilChanged, filter, map, type Subscription} from 'rxjs'
|
|
|
3
3
|
import {type StoreContext} from '../store/defineStore'
|
|
4
4
|
import {AuthStateType} from './authStateType'
|
|
5
5
|
import {type AuthStoreState} from './authStore'
|
|
6
|
-
import {getStorageEvents, getTokenFromStorage} from './utils'
|
|
6
|
+
import {createLoggedInAuthState, getStorageEvents, getTokenFromStorage} from './utils'
|
|
7
7
|
|
|
8
8
|
export const subscribeToStorageEventsAndSetToken = ({
|
|
9
9
|
state,
|
|
@@ -22,7 +22,7 @@ export const subscribeToStorageEventsAndSetToken = ({
|
|
|
22
22
|
return tokenFromStorage$.subscribe((token) => {
|
|
23
23
|
state.set('updateTokenFromStorageEvent', {
|
|
24
24
|
authState: token
|
|
25
|
-
?
|
|
25
|
+
? createLoggedInAuthState(token, null)
|
|
26
26
|
: {type: AuthStateType.LOGGED_OUT, isDestroyingSession: false},
|
|
27
27
|
})
|
|
28
28
|
})
|
package/src/auth/utils.ts
CHANGED
|
@@ -1,7 +1,40 @@
|
|
|
1
1
|
import {type ClientError} from '@sanity/client'
|
|
2
|
+
import {type CurrentUser} from '@sanity/types'
|
|
2
3
|
import {EMPTY, fromEvent, Observable} from 'rxjs'
|
|
3
4
|
|
|
4
5
|
import {AUTH_CODE_PARAM, DEFAULT_BASE} from './authConstants'
|
|
6
|
+
import {AuthStateType} from './authStateType'
|
|
7
|
+
import {type LoggedInAuthState} from './authStore'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Creates a properly initialized {@link LoggedInAuthState}.
|
|
11
|
+
*
|
|
12
|
+
* For stamped tokens (containing `"-st"`), `lastTokenRefresh` is set to
|
|
13
|
+
* `Date.now()` so that the visibility-change handler in
|
|
14
|
+
* {@link refreshStampedToken} does not trigger an unnecessary refresh the
|
|
15
|
+
* first time the tab becomes visible.
|
|
16
|
+
*
|
|
17
|
+
* @param token - The auth token.
|
|
18
|
+
* @param currentUser - The current user, or `null` if not yet fetched.
|
|
19
|
+
* @param existingLastTokenRefresh - An existing timestamp to preserve
|
|
20
|
+
* (e.g. when updating a token while keeping the previous refresh time).
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
export function createLoggedInAuthState(
|
|
24
|
+
token: string,
|
|
25
|
+
currentUser: CurrentUser | null,
|
|
26
|
+
existingLastTokenRefresh?: number,
|
|
27
|
+
): LoggedInAuthState {
|
|
28
|
+
const isStampedToken = token.includes('-st')
|
|
29
|
+
const lastTokenRefresh = existingLastTokenRefresh ?? (isStampedToken ? Date.now() : undefined)
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
type: AuthStateType.LOGGED_IN,
|
|
33
|
+
token,
|
|
34
|
+
currentUser,
|
|
35
|
+
...(lastTokenRefresh !== undefined && {lastTokenRefresh}),
|
|
36
|
+
}
|
|
37
|
+
}
|
|
5
38
|
|
|
6
39
|
export function getAuthCode(callbackUrl: string | undefined, locationHref: string): string | null {
|
|
7
40
|
const loc = new URL(locationHref, DEFAULT_BASE)
|
|
@@ -68,6 +68,20 @@ describe('clientStore', () => {
|
|
|
68
68
|
})
|
|
69
69
|
})
|
|
70
70
|
|
|
71
|
+
it('should pass staging apiHost when __SANITY_STAGING__ is true and no explicit apiHost', () => {
|
|
72
|
+
vi.stubGlobal('__SANITY_STAGING__', true)
|
|
73
|
+
|
|
74
|
+
getClient(instance, {apiVersion: '2024-11-12'})
|
|
75
|
+
|
|
76
|
+
expect(vi.mocked(createClient)).toHaveBeenCalledWith(
|
|
77
|
+
expect.objectContaining({
|
|
78
|
+
apiHost: 'https://api.sanity.work',
|
|
79
|
+
}),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
vi.unstubAllGlobals()
|
|
83
|
+
})
|
|
84
|
+
|
|
71
85
|
it('should throw when using disallowed configuration keys', () => {
|
|
72
86
|
expect(() =>
|
|
73
87
|
getClient(instance, {
|
|
@@ -176,17 +190,17 @@ describe('clientStore', () => {
|
|
|
176
190
|
})
|
|
177
191
|
})
|
|
178
192
|
|
|
179
|
-
describe('
|
|
180
|
-
it('should create client when
|
|
193
|
+
describe('resource handling', () => {
|
|
194
|
+
it('should create client when resource is provided', () => {
|
|
181
195
|
const client = getClient(instance, {
|
|
182
196
|
apiVersion: '2024-11-12',
|
|
183
|
-
|
|
197
|
+
resource: {projectId: 'source-project', dataset: 'source-dataset'},
|
|
184
198
|
})
|
|
185
199
|
|
|
186
200
|
expect(vi.mocked(createClient)).toHaveBeenCalledWith(
|
|
187
201
|
expect.objectContaining({
|
|
188
|
-
|
|
189
|
-
|
|
202
|
+
apiVersion: '2024-11-12',
|
|
203
|
+
resource: {type: 'dataset', id: 'source-project.source-dataset'},
|
|
190
204
|
}),
|
|
191
205
|
)
|
|
192
206
|
// Client should be projectless - no projectId/dataset in config
|
|
@@ -194,21 +208,21 @@ describe('clientStore', () => {
|
|
|
194
208
|
expect(client.config()).not.toHaveProperty('dataset')
|
|
195
209
|
expect(client.config()).toEqual(
|
|
196
210
|
expect.objectContaining({
|
|
197
|
-
|
|
211
|
+
resource: {type: 'dataset', id: 'source-project.source-dataset'},
|
|
198
212
|
}),
|
|
199
213
|
)
|
|
200
214
|
})
|
|
201
215
|
|
|
202
|
-
it('should create resource when
|
|
216
|
+
it('should create resource when resource has array resourceId and be projectless', () => {
|
|
203
217
|
const client = getClient(instance, {
|
|
204
218
|
apiVersion: '2024-11-12',
|
|
205
|
-
|
|
219
|
+
resource: {mediaLibraryId: 'media-lib-123'},
|
|
206
220
|
})
|
|
207
221
|
|
|
208
222
|
expect(vi.mocked(createClient)).toHaveBeenCalledWith(
|
|
209
223
|
expect.objectContaining({
|
|
210
|
-
|
|
211
|
-
|
|
224
|
+
resource: {type: 'media-library', id: 'media-lib-123'},
|
|
225
|
+
apiVersion: '2024-11-12',
|
|
212
226
|
}),
|
|
213
227
|
)
|
|
214
228
|
// Client should be projectless - no projectId/dataset in config
|
|
@@ -216,21 +230,21 @@ describe('clientStore', () => {
|
|
|
216
230
|
expect(client.config()).not.toHaveProperty('dataset')
|
|
217
231
|
expect(client.config()).toEqual(
|
|
218
232
|
expect.objectContaining({
|
|
219
|
-
|
|
233
|
+
resource: {type: 'media-library', id: 'media-lib-123'},
|
|
220
234
|
}),
|
|
221
235
|
)
|
|
222
236
|
})
|
|
223
237
|
|
|
224
|
-
it('should create resource when canvas
|
|
238
|
+
it('should create resource when canvas resource is provided and be projectless', () => {
|
|
225
239
|
const client = getClient(instance, {
|
|
226
240
|
apiVersion: '2024-11-12',
|
|
227
|
-
|
|
241
|
+
resource: {canvasId: 'canvas-123'},
|
|
228
242
|
})
|
|
229
243
|
|
|
230
244
|
expect(vi.mocked(createClient)).toHaveBeenCalledWith(
|
|
231
245
|
expect.objectContaining({
|
|
232
|
-
|
|
233
|
-
|
|
246
|
+
resource: {type: 'canvas', id: 'canvas-123'},
|
|
247
|
+
apiVersion: '2024-11-12',
|
|
234
248
|
}),
|
|
235
249
|
)
|
|
236
250
|
// Client should be projectless - no projectId/dataset in config
|
|
@@ -238,38 +252,38 @@ describe('clientStore', () => {
|
|
|
238
252
|
expect(client.config()).not.toHaveProperty('dataset')
|
|
239
253
|
expect(client.config()).toEqual(
|
|
240
254
|
expect.objectContaining({
|
|
241
|
-
|
|
255
|
+
resource: {type: 'canvas', id: 'canvas-123'},
|
|
242
256
|
}),
|
|
243
257
|
)
|
|
244
258
|
})
|
|
245
259
|
|
|
246
|
-
it('should create projectless client when
|
|
260
|
+
it('should create projectless client when resource is provided, ignoring instance config', () => {
|
|
247
261
|
const client = getClient(instance, {
|
|
248
262
|
apiVersion: '2024-11-12',
|
|
249
|
-
|
|
263
|
+
resource: {projectId: 'source-project', dataset: 'source-dataset'},
|
|
250
264
|
})
|
|
251
265
|
|
|
252
|
-
// Client should be projectless -
|
|
266
|
+
// Client should be projectless - resource takes precedence, instance config is ignored
|
|
253
267
|
expect(client.config()).not.toHaveProperty('projectId')
|
|
254
268
|
expect(client.config()).not.toHaveProperty('dataset')
|
|
255
269
|
expect(client.config()).toEqual(
|
|
256
270
|
expect.objectContaining({
|
|
257
|
-
|
|
271
|
+
resource: {type: 'dataset', id: 'source-project.source-dataset'},
|
|
258
272
|
}),
|
|
259
273
|
)
|
|
260
274
|
})
|
|
261
275
|
|
|
262
|
-
it('should warn when both
|
|
276
|
+
it('should warn when both resource and explicit projectId/dataset are provided', () => {
|
|
263
277
|
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
264
278
|
const client = getClient(instance, {
|
|
265
279
|
apiVersion: '2024-11-12',
|
|
266
|
-
|
|
280
|
+
resource: {projectId: 'source-project', dataset: 'source-dataset'},
|
|
267
281
|
projectId: 'explicit-project',
|
|
268
282
|
dataset: 'explicit-dataset',
|
|
269
283
|
})
|
|
270
284
|
|
|
271
285
|
expect(consoleSpy).toHaveBeenCalledWith(
|
|
272
|
-
'Both
|
|
286
|
+
'Both resource and explicit projectId/dataset are provided. The resource will be used and projectId/dataset will be ignored.',
|
|
273
287
|
)
|
|
274
288
|
// Client should still be projectless despite explicit projectId/dataset
|
|
275
289
|
expect(client.config()).not.toHaveProperty('projectId')
|
|
@@ -277,18 +291,18 @@ describe('clientStore', () => {
|
|
|
277
291
|
consoleSpy.mockRestore()
|
|
278
292
|
})
|
|
279
293
|
|
|
280
|
-
it('should create different clients for different
|
|
294
|
+
it('should create different clients for different resources', () => {
|
|
281
295
|
const client1 = getClient(instance, {
|
|
282
296
|
apiVersion: '2024-11-12',
|
|
283
|
-
|
|
297
|
+
resource: {projectId: 'source-project', dataset: 'source-dataset'},
|
|
284
298
|
})
|
|
285
299
|
const client2 = getClient(instance, {
|
|
286
300
|
apiVersion: '2024-11-12',
|
|
287
|
-
|
|
301
|
+
resource: {mediaLibraryId: 'media-lib-123'},
|
|
288
302
|
})
|
|
289
303
|
const client3 = getClient(instance, {
|
|
290
304
|
apiVersion: '2024-11-12',
|
|
291
|
-
|
|
305
|
+
resource: {canvasId: 'canvas-123'},
|
|
292
306
|
})
|
|
293
307
|
|
|
294
308
|
expect(client1).not.toBe(client2)
|
|
@@ -297,14 +311,14 @@ describe('clientStore', () => {
|
|
|
297
311
|
expect(vi.mocked(createClient)).toHaveBeenCalledTimes(3)
|
|
298
312
|
})
|
|
299
313
|
|
|
300
|
-
it('should reuse clients with identical
|
|
314
|
+
it('should reuse clients with identical resource configurations', () => {
|
|
301
315
|
const client1 = getClient(instance, {
|
|
302
316
|
apiVersion: '2024-11-12',
|
|
303
|
-
|
|
317
|
+
resource: {projectId: 'source-project', dataset: 'source-dataset'},
|
|
304
318
|
})
|
|
305
319
|
const client2 = getClient(instance, {
|
|
306
320
|
apiVersion: '2024-11-12',
|
|
307
|
-
|
|
321
|
+
resource: {projectId: 'source-project', dataset: 'source-dataset'},
|
|
308
322
|
})
|
|
309
323
|
|
|
310
324
|
expect(client1).toBe(client2)
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import {type ClientConfig, createClient, type SanityClient} from '@sanity/client'
|
|
2
|
-
import {pick} from 'lodash-es'
|
|
3
2
|
|
|
4
3
|
import {getAuthMethodState, getTokenState} from '../auth/authStore'
|
|
5
4
|
import {
|
|
6
|
-
type
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
type DocumentResource,
|
|
6
|
+
isCanvasResource,
|
|
7
|
+
isDatasetResource,
|
|
8
|
+
isMediaLibraryResource,
|
|
10
9
|
} from '../config/sanityConfig'
|
|
11
10
|
import {bindActionGlobally} from '../store/createActionBinder'
|
|
12
11
|
import {createStateSourceAction} from '../store/createStateSourceAction'
|
|
13
12
|
import {defineStore, type StoreContext} from '../store/defineStore'
|
|
13
|
+
import {getStagingApiHost} from '../utils/getStagingApiHost'
|
|
14
|
+
import {pickProperties} from '../utils/object'
|
|
14
15
|
|
|
15
16
|
const DEFAULT_API_VERSION = '2024-11-12'
|
|
16
17
|
const DEFAULT_REQUEST_TAG_PREFIX = 'sanity.sdk'
|
|
@@ -30,22 +31,21 @@ type AllowedClientConfigKey =
|
|
|
30
31
|
| 'useProjectHostname'
|
|
31
32
|
|
|
32
33
|
const allowedKeys = Object.keys({
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
'source': null,
|
|
34
|
+
apiHost: null,
|
|
35
|
+
useCdn: null,
|
|
36
|
+
token: null,
|
|
37
|
+
perspective: null,
|
|
38
|
+
proxy: null,
|
|
39
|
+
withCredentials: null,
|
|
40
|
+
timeout: null,
|
|
41
|
+
maxRetries: null,
|
|
42
|
+
dataset: null,
|
|
43
|
+
projectId: null,
|
|
44
|
+
scope: null,
|
|
45
|
+
apiVersion: null,
|
|
46
|
+
requestTagPrefix: null,
|
|
47
|
+
useProjectHostname: null,
|
|
48
|
+
resource: null,
|
|
49
49
|
} satisfies Record<keyof ClientOptions, null>) as (keyof ClientOptions)[]
|
|
50
50
|
|
|
51
51
|
const DEFAULT_CLIENT_CONFIG: ClientConfig = {
|
|
@@ -66,11 +66,6 @@ export interface ClientStoreState {
|
|
|
66
66
|
authMethod?: 'localstorage' | 'cookie'
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
interface ClientResource {
|
|
70
|
-
type: 'dataset' | 'media-library' | 'canvas'
|
|
71
|
-
id: string
|
|
72
|
-
}
|
|
73
|
-
|
|
74
69
|
/**
|
|
75
70
|
* Options used when retrieving a client instance from the client store.
|
|
76
71
|
*
|
|
@@ -93,20 +88,17 @@ export interface ClientOptions extends Pick<ClientConfig, AllowedClientConfigKey
|
|
|
93
88
|
* and the global client ('global'). When set to `'global'`, the global client
|
|
94
89
|
* is used.
|
|
95
90
|
*/
|
|
96
|
-
|
|
91
|
+
scope?: 'default' | 'global'
|
|
97
92
|
/**
|
|
98
93
|
* A required string indicating the API version for the client.
|
|
99
94
|
*/
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* @internal
|
|
103
|
-
*/
|
|
104
|
-
'~experimental_resource'?: ClientConfig['~experimental_resource']
|
|
95
|
+
apiVersion: string
|
|
105
96
|
|
|
106
97
|
/**
|
|
107
98
|
* @internal
|
|
99
|
+
* The SDK resource to use for the client -- this will get transformed into a ClientConfig resource.
|
|
108
100
|
*/
|
|
109
|
-
|
|
101
|
+
resource?: DocumentResource
|
|
110
102
|
}
|
|
111
103
|
|
|
112
104
|
const clientStore = defineStore<ClientStoreState>({
|
|
@@ -143,7 +135,13 @@ const listenToAuthMethod = ({instance, state}: StoreContext<ClientStoreState>) =
|
|
|
143
135
|
})
|
|
144
136
|
}
|
|
145
137
|
|
|
146
|
-
|
|
138
|
+
type ClientInstanceCacheKeyInput = ClientConfig &
|
|
139
|
+
Partial<Pick<ClientOptions, 'scope'>> & {
|
|
140
|
+
apiVersion: string
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const getClientConfigKey = (options: ClientInstanceCacheKeyInput) =>
|
|
144
|
+
JSON.stringify(pickProperties(options, allowedKeys))
|
|
147
145
|
|
|
148
146
|
/**
|
|
149
147
|
* Retrieves a Sanity client instance configured with the provided options.
|
|
@@ -181,41 +179,44 @@ export const getClient = bindActionGlobally(
|
|
|
181
179
|
const tokenFromState = state.get().token
|
|
182
180
|
const {clients, authMethod} = state.get()
|
|
183
181
|
|
|
184
|
-
let resource:
|
|
182
|
+
let resource: ClientConfig['resource'] | undefined
|
|
185
183
|
|
|
186
|
-
if (options.
|
|
187
|
-
if (
|
|
188
|
-
resource = {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
184
|
+
if (options.resource) {
|
|
185
|
+
if (isDatasetResource(options.resource)) {
|
|
186
|
+
resource = {
|
|
187
|
+
type: 'dataset',
|
|
188
|
+
id: `${options.resource.projectId}.${options.resource.dataset}`,
|
|
189
|
+
}
|
|
190
|
+
} else if (isMediaLibraryResource(options.resource)) {
|
|
191
|
+
resource = {type: 'media-library', id: options.resource.mediaLibraryId}
|
|
192
|
+
} else if (isCanvasResource(options.resource)) {
|
|
193
|
+
resource = {type: 'canvas', id: options.resource.canvasId}
|
|
193
194
|
}
|
|
194
195
|
}
|
|
195
196
|
|
|
196
197
|
const projectId = options.projectId ?? instance.config.projectId
|
|
197
198
|
const dataset = options.dataset ?? instance.config.dataset
|
|
198
|
-
const apiHost = options.apiHost ?? instance.config.auth?.apiHost
|
|
199
|
+
const apiHost = options.apiHost ?? instance.config.auth?.apiHost ?? getStagingApiHost()
|
|
199
200
|
|
|
200
|
-
const effectiveOptions:
|
|
201
|
+
const effectiveOptions: ClientConfig & {apiVersion: string} = {
|
|
201
202
|
...DEFAULT_CLIENT_CONFIG,
|
|
202
203
|
...((options.scope === 'global' || !projectId || resource) && {useProjectHostname: false}),
|
|
203
204
|
token: authMethod === 'cookie' ? undefined : (tokenFromState ?? undefined),
|
|
204
205
|
...options,
|
|
205
206
|
...(projectId && {projectId}),
|
|
206
207
|
...(dataset && {dataset}),
|
|
208
|
+
...(resource ? {resource} : {resource: undefined}),
|
|
207
209
|
...(apiHost && {apiHost}),
|
|
208
|
-
...(resource && {'~experimental_resource': resource}),
|
|
209
210
|
}
|
|
210
211
|
|
|
211
|
-
// When a
|
|
212
|
-
// The client code itself will ignore the non-
|
|
212
|
+
// When a resource is provided, don't use projectId/dataset - the client should be "projectless"
|
|
213
|
+
// The client code itself will ignore the non-resource config, so we do this to prevent confusing the user.
|
|
213
214
|
// (ref: https://github.com/sanity-io/client/blob/5c23f81f5ab93a53f5b22b39845c867988508d84/src/data/dataMethods.ts#L691)
|
|
214
215
|
if (resource) {
|
|
215
216
|
if (options.projectId || options.dataset) {
|
|
216
217
|
// eslint-disable-next-line no-console
|
|
217
218
|
console.warn(
|
|
218
|
-
'Both
|
|
219
|
+
'Both resource and explicit projectId/dataset are provided. The resource will be used and projectId/dataset will be ignored.',
|
|
219
220
|
)
|
|
220
221
|
}
|
|
221
222
|
delete effectiveOptions.projectId
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {type ChannelInput, type ChannelInstance} from '@sanity/comlink'
|
|
2
|
-
import {isEqual} from 'lodash-es'
|
|
3
2
|
|
|
4
3
|
import {type StoreContext} from '../../../store/defineStore'
|
|
4
|
+
import {isDeepEqual} from '../../../utils/object'
|
|
5
5
|
import {type FrameMessage, type WindowMessage} from '../../types'
|
|
6
6
|
import {type ComlinkControllerState} from '../comlinkControllerStore'
|
|
7
7
|
|
|
@@ -25,7 +25,7 @@ export const getOrCreateChannel = (
|
|
|
25
25
|
|
|
26
26
|
// limit channels to one per name
|
|
27
27
|
if (existing) {
|
|
28
|
-
if (!
|
|
28
|
+
if (!isDeepEqual(existing.options, options)) {
|
|
29
29
|
throw new Error(`Channel "${options.name}" already exists with different options`)
|
|
30
30
|
}
|
|
31
31
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {createNode, type Node, type NodeInput} from '@sanity/comlink'
|
|
2
|
-
import {isEqual} from 'lodash-es'
|
|
3
2
|
|
|
4
3
|
import {type StoreContext} from '../../../store/defineStore'
|
|
4
|
+
import {isDeepEqual} from '../../../utils/object'
|
|
5
5
|
import {type FrameMessage, type WindowMessage} from '../../types'
|
|
6
6
|
import {type ComlinkNodeState} from '../comlinkNodeStore'
|
|
7
7
|
|
|
@@ -14,7 +14,7 @@ export const getOrCreateNode = (
|
|
|
14
14
|
|
|
15
15
|
// limit nodes to one per name
|
|
16
16
|
if (existing) {
|
|
17
|
-
if (!
|
|
17
|
+
if (!isDeepEqual(existing.options, options)) {
|
|
18
18
|
throw new Error(`Node "${options.name}" already exists with different options`)
|
|
19
19
|
}
|
|
20
20
|
|
|
@@ -3,6 +3,7 @@ import {createSelector} from 'reselect'
|
|
|
3
3
|
|
|
4
4
|
import {bindActionGlobally} from '../../store/createActionBinder'
|
|
5
5
|
import {createStateSourceAction, type SelectorContext} from '../../store/createStateSourceAction'
|
|
6
|
+
import {setCleanupTimeout} from '../../utils/setCleanupTimeout'
|
|
6
7
|
import {type FrameMessage, type WindowMessage} from '../types'
|
|
7
8
|
import {
|
|
8
9
|
type ComlinkNodeState,
|
|
@@ -57,7 +58,7 @@ export const getNodeState = bindActionGlobally(
|
|
|
57
58
|
subs.add(subscriberId)
|
|
58
59
|
|
|
59
60
|
return () => {
|
|
60
|
-
|
|
61
|
+
setCleanupTimeout(() => {
|
|
61
62
|
const activeSubs = state.get().subscriptions.get(nodeName)
|
|
62
63
|
if (activeSubs) {
|
|
63
64
|
activeSubs.delete(subscriberId)
|