@sanity/sdk 2.2.0 → 2.3.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/sdk",
3
- "version": "2.2.0",
3
+ "version": "2.3.1",
4
4
  "private": false,
5
5
  "description": "Sanity SDK",
6
6
  "keywords": [
@@ -43,7 +43,7 @@
43
43
  "prettier": "@sanity/prettier-config",
44
44
  "dependencies": {
45
45
  "@sanity/bifur-client": "^0.4.1",
46
- "@sanity/client": "^7.10.0",
46
+ "@sanity/client": "^7.12.0",
47
47
  "@sanity/comlink": "^3.0.4",
48
48
  "@sanity/diff-match-patch": "^3.2.0",
49
49
  "@sanity/diff-patch": "^6.0.0",
@@ -52,6 +52,7 @@
52
52
  "@sanity/mutate": "^0.12.4",
53
53
  "@sanity/types": "^3.83.0",
54
54
  "groq": "3.88.1-typegen-experimental.0",
55
+ "groq-js": "^1.19.0",
55
56
  "lodash-es": "^4.17.21",
56
57
  "reselect": "^5.1.1",
57
58
  "rxjs": "^7.8.2",
@@ -64,17 +65,16 @@
64
65
  "@types/lodash-es": "^4.17.12",
65
66
  "@vitest/coverage-v8": "3.1.2",
66
67
  "eslint": "^9.22.0",
67
- "groq-js": "^1.16.1",
68
68
  "prettier": "^3.5.3",
69
69
  "rollup-plugin-visualizer": "^5.14.0",
70
70
  "typescript": "^5.8.3",
71
71
  "vite": "^6.3.4",
72
72
  "vitest": "^3.1.2",
73
73
  "@repo/config-eslint": "0.0.0",
74
- "@repo/config-test": "0.0.1",
75
- "@repo/package.config": "0.0.1",
76
74
  "@repo/package.bundle": "3.82.0",
77
- "@repo/tsconfig": "0.0.1"
75
+ "@repo/package.config": "0.0.1",
76
+ "@repo/tsconfig": "0.0.1",
77
+ "@repo/config-test": "0.0.1"
78
78
  },
79
79
  "engines": {
80
80
  "node": ">=20.0.0"
@@ -157,6 +157,7 @@ export {
157
157
  export {type FetcherStore, type FetcherStoreState} from '../utils/createFetcherStore'
158
158
  export {createGroqSearchFilter} from '../utils/createGroqSearchFilter'
159
159
  export {defineIntent, type Intent, type IntentFilter} from '../utils/defineIntent'
160
+ export {getCorsErrorProjectId} from '../utils/getCorsErrorProjectId'
160
161
  export {CORE_SDK_VERSION} from '../version'
161
162
  export {
162
163
  getIndexForKey,
@@ -237,6 +237,7 @@ describe('authStore', () => {
237
237
  it('sets to logged in using studio token when studio mode is enabled and token exists', () => {
238
238
  const studioToken = 'studio-token'
239
239
  const projectId = 'studio-project'
240
+ const studioStorageKey = `__studio_auth_token_${projectId}`
240
241
  const mockStorage = {
241
242
  getItem: vi.fn(),
242
243
  setItem: vi.fn(),
@@ -252,40 +253,38 @@ describe('authStore', () => {
252
253
  })
253
254
 
254
255
  const {authState, options} = authStore.getInitialState(instance)
255
- expect(getStudioTokenFromLocalStorage).toHaveBeenCalledWith(mockStorage, projectId)
256
+ expect(getStudioTokenFromLocalStorage).toHaveBeenCalledWith(mockStorage, studioStorageKey)
256
257
  expect(authState).toMatchObject({type: AuthStateType.LOGGED_IN, token: studioToken})
257
258
  expect(options.authMethod).toBe('localstorage')
258
259
  })
259
260
 
260
- it('checks for cookie auth when studio mode is enabled and no studio token exists', async () => {
261
- vi.useFakeTimers()
261
+ it('checks for cookie auth during initialize when studio mode is enabled and no studio token exists', () => {
262
262
  const projectId = 'studio-project'
263
+ const studioStorageKey = `__studio_auth_token_${projectId}`
263
264
  const mockStorage = {
264
265
  getItem: vi.fn(),
265
266
  setItem: vi.fn(),
266
267
  removeItem: vi.fn(),
267
268
  } as unknown as Storage // Mock storage
268
269
  vi.mocked(getStudioTokenFromLocalStorage).mockReturnValue(null)
269
- // Mock cookie check to return true asynchronously
270
270
  vi.mocked(checkForCookieAuth).mockResolvedValue(true)
271
271
 
272
272
  instance = createSanityInstance({
273
273
  projectId,
274
274
  dataset: 'd',
275
275
  studioMode: {enabled: true},
276
- auth: {storageArea: mockStorage}, // Provide mock storage
276
+ auth: {storageArea: mockStorage},
277
277
  })
278
278
 
279
- // Initial state might be logged out before the async check completes
279
+ // Verify initial state without async cookie probe
280
280
  const {authState: initialAuthState} = authStore.getInitialState(instance)
281
- expect(initialAuthState.type).toBe(AuthStateType.LOGGED_OUT) // Or potentially logging in depending on other factors
282
- expect(getStudioTokenFromLocalStorage).toHaveBeenCalledWith(mockStorage, projectId)
283
- expect(checkForCookieAuth).toHaveBeenCalledWith(projectId, expect.any(Function))
281
+ expect(initialAuthState.type).toBe(AuthStateType.LOGGED_OUT)
282
+ expect(getStudioTokenFromLocalStorage).toHaveBeenCalledWith(mockStorage, studioStorageKey)
284
283
 
285
- // Wait for the promise in getInitialState to resolve
286
- await vi.runAllTimersAsync()
284
+ // Trigger store creation + initialize
285
+ getAuthState(instance)
287
286
 
288
- vi.useRealTimers()
287
+ expect(checkForCookieAuth).toHaveBeenCalledWith(projectId, expect.any(Function))
289
288
  })
290
289
 
291
290
  it('falls back to default auth (storage token) when studio mode is disabled', () => {
@@ -573,14 +572,7 @@ describe('authStore', () => {
573
572
  expect(initialOrgId.getCurrent()).toBe('initial-org-id')
574
573
 
575
574
  // Call handleCallback with the callback URL
576
- await handleAuthCallback(instance, callbackUrl) // Use await as handleAuthCallback is async
577
-
578
- // Wait for the state update to be reflected in the selector
579
- await vi.waitUntil(
580
- () => getDashboardOrganizationId(instance).getCurrent() === 'callback-org-id',
581
- )
582
- // Add a microtask yield just in case
583
- await new Promise((resolve) => setTimeout(resolve, 0))
575
+ await handleAuthCallback(instance, callbackUrl)
584
576
 
585
577
  // Check that the orgId from the callback context is now set
586
578
  const finalOrgId = getDashboardOrganizationId(instance)
@@ -67,7 +67,11 @@ export interface DashboardContext {
67
67
  orgId?: string
68
68
  }
69
69
 
70
- type AuthMethodOptions = 'localstorage' | 'cookie' | undefined
70
+ /**
71
+ * The method of authentication used.
72
+ * @internal
73
+ */
74
+ export type AuthMethodOptions = 'localstorage' | 'cookie' | undefined
71
75
 
72
76
  let tokenRefresherRunning = false
73
77
 
@@ -105,7 +109,8 @@ export const authStore = defineStore<AuthStoreState>({
105
109
  } = instance.config.auth ?? {}
106
110
  let storageArea = instance.config.auth?.storageArea
107
111
 
108
- const storageKey = `__sanity_auth_token`
112
+ let storageKey = `__sanity_auth_token`
113
+ const studioModeEnabled = instance.config.studioMode?.enabled
109
114
 
110
115
  // This login URL will only be used for local development
111
116
  let loginDomain = 'https://www.sanity.io'
@@ -149,23 +154,21 @@ export const authStore = defineStore<AuthStoreState>({
149
154
  console.error('Failed to parse dashboard context from initial location:', err)
150
155
  }
151
156
 
152
- if (!isInDashboard) {
157
+ if (!isInDashboard || studioModeEnabled) {
153
158
  // If not in dashboard, use the storage area from the config
159
+ // If studio mode is enabled, use the local storage area (default)
154
160
  storageArea = storageArea ?? getDefaultStorage()
155
161
  }
156
162
 
157
163
  let token: string | null
158
164
  let authMethod: AuthMethodOptions
159
- if (instance.config.studioMode?.enabled) {
160
- token = getStudioTokenFromLocalStorage(storageArea, instance.config.projectId)
165
+ if (studioModeEnabled) {
166
+ // In studio mode, always use the studio-specific storage key and subscribe to it
167
+ const studioStorageKey = `__studio_auth_token_${instance.config.projectId ?? ''}`
168
+ storageKey = studioStorageKey
169
+ token = getStudioTokenFromLocalStorage(storageArea, studioStorageKey)
161
170
  if (token) {
162
171
  authMethod = 'localstorage'
163
- } else {
164
- checkForCookieAuth(instance.config.projectId, clientFactory).then((isCookieAuthEnabled) => {
165
- if (isCookieAuthEnabled) {
166
- authMethod = 'cookie'
167
- }
168
- })
169
172
  }
170
173
  } else {
171
174
  token = getTokenFromStorage(storageArea, storageKey)
@@ -177,14 +180,16 @@ export const authStore = defineStore<AuthStoreState>({
177
180
  let authState: AuthState
178
181
  if (providedToken) {
179
182
  authState = {type: AuthStateType.LOGGED_IN, token: providedToken, currentUser: null}
183
+ } else if (token && studioModeEnabled) {
184
+ authState = {type: AuthStateType.LOGGED_IN, token: token ?? '', currentUser: null}
180
185
  } else if (
181
186
  getAuthCode(callbackUrl, initialLocationHref) ||
182
187
  getTokenFromLocation(initialLocationHref)
183
188
  ) {
184
189
  authState = {type: AuthStateType.LOGGING_IN, isExchangingToken: false}
185
190
  // Note: dashboardContext from the callback URL can be set later in handleAuthCallback too
186
- } else if (token && !isInDashboard) {
187
- // Only use token from storage if NOT running in dashboard
191
+ } else if (token && !isInDashboard && !studioModeEnabled) {
192
+ // Only use token from storage if NOT running in dashboard and studio mode is not enabled
188
193
  authState = {type: AuthStateType.LOGGED_IN, token, currentUser: null}
189
194
  } else {
190
195
  // Default to logged out if no provided token, not handling callback,
@@ -212,11 +217,37 @@ export const authStore = defineStore<AuthStoreState>({
212
217
  initialize(context) {
213
218
  const subscriptions: Subscription[] = []
214
219
  subscriptions.push(subscribeToStateAndFetchCurrentUser(context))
215
-
216
- if (context.state.get().options?.storageArea) {
220
+ const storageArea = context.state.get().options?.storageArea
221
+ if (storageArea) {
217
222
  subscriptions.push(subscribeToStorageEventsAndSetToken(context))
218
223
  }
219
224
 
225
+ // If in Studio mode with no local token, resolve cookie auth asynchronously
226
+ try {
227
+ const {instance, state} = context
228
+ const studioModeEnabled = !!instance.config.studioMode?.enabled
229
+ const token: string | null =
230
+ state.get().authState?.type === AuthStateType.LOGGED_IN
231
+ ? (state.get().authState as LoggedInAuthState).token
232
+ : null
233
+ if (studioModeEnabled && !token) {
234
+ const projectId = instance.config.projectId
235
+ const clientFactory = state.get().options.clientFactory
236
+ checkForCookieAuth(projectId, clientFactory).then((isCookieAuthEnabled) => {
237
+ if (!isCookieAuthEnabled) return
238
+ state.set('enableCookieAuth', (prev) => ({
239
+ options: {...prev.options, authMethod: 'cookie'},
240
+ authState:
241
+ prev.authState.type === AuthStateType.LOGGED_IN
242
+ ? prev.authState
243
+ : {type: AuthStateType.LOGGED_IN, token: '', currentUser: null},
244
+ }))
245
+ })
246
+ }
247
+ } catch {
248
+ // best-effort cookie detection
249
+ }
250
+
220
251
  if (!tokenRefresherRunning) {
221
252
  tokenRefresherRunning = true
222
253
  subscriptions.push(refreshStampedToken(context))
@@ -96,7 +96,7 @@ describe('getStudioTokenFromLocalStorage', () => {
96
96
  expect(getTokenFromStorageSpy).not.toHaveBeenCalled()
97
97
  })
98
98
 
99
- it('should return null if projectId is undefined', () => {
99
+ it('should return null if storageKey is undefined', () => {
100
100
  const result = getStudioTokenFromLocalStorage(storageArea, undefined)
101
101
  expect(result).toBeNull()
102
102
  expect(getTokenFromStorageSpy).not.toHaveBeenCalled()
@@ -104,19 +104,19 @@ describe('getStudioTokenFromLocalStorage', () => {
104
104
 
105
105
  it('should call getTokenFromStorage with correct key', () => {
106
106
  getTokenFromStorageSpy.mockReturnValue(null) // Assume token not found for this test
107
- getStudioTokenFromLocalStorage(storageArea, projectId)
107
+ getStudioTokenFromLocalStorage(storageArea, studioStorageKey)
108
108
  expect(getTokenFromStorageSpy).toHaveBeenCalledWith(storageArea, studioStorageKey)
109
109
  })
110
110
 
111
111
  it('should return the token if found in storage', () => {
112
112
  getTokenFromStorageSpy.mockReturnValue(mockToken)
113
- const result = getStudioTokenFromLocalStorage(storageArea, projectId)
113
+ const result = getStudioTokenFromLocalStorage(storageArea, studioStorageKey)
114
114
  expect(result).toBe(mockToken)
115
115
  })
116
116
 
117
117
  it('should return null if token is not found in storage', () => {
118
118
  getTokenFromStorageSpy.mockReturnValue(null)
119
- const result = getStudioTokenFromLocalStorage(storageArea, projectId)
119
+ const result = getStudioTokenFromLocalStorage(storageArea, studioStorageKey)
120
120
  expect(result).toBeNull()
121
121
  })
122
122
  })
@@ -33,17 +33,16 @@ export async function checkForCookieAuth(
33
33
  /**
34
34
  * Attempts to retrieve a studio token from local storage.
35
35
  * @param storageArea - The storage area to retrieve the token from.
36
- * @param projectId - The project ID to retrieve the token for.
36
+ * @param storageKey - The storage key to retrieve the token from.
37
37
  * @returns The studio token or null if it does not exist.
38
38
  * @internal
39
39
  */
40
40
  export function getStudioTokenFromLocalStorage(
41
41
  storageArea: Storage | undefined,
42
- projectId: string | undefined,
42
+ storageKey: string | undefined,
43
43
  ): string | null {
44
- if (!storageArea || !projectId) return null
45
- const studioStorageKey = `__studio_auth_token_${projectId}`
46
- const token = getTokenFromStorage(storageArea, studioStorageKey)
44
+ if (!storageArea || !storageKey) return null
45
+ const token = getTokenFromStorage(storageArea, storageKey)
47
46
  if (token) {
48
47
  return token
49
48
  }
@@ -4,32 +4,43 @@ import {distinctUntilChanged, filter, map, type Subscription, switchMap} from 'r
4
4
  import {type StoreContext} from '../store/defineStore'
5
5
  import {DEFAULT_API_VERSION, REQUEST_TAG_PREFIX} from './authConstants'
6
6
  import {AuthStateType} from './authStateType'
7
- import {type AuthState, type AuthStoreState} from './authStore'
7
+ import {type AuthMethodOptions, type AuthState, type AuthStoreState} from './authStore'
8
8
 
9
9
  export const subscribeToStateAndFetchCurrentUser = ({
10
10
  state,
11
+ instance,
11
12
  }: StoreContext<AuthStoreState>): Subscription => {
12
13
  const {clientFactory, apiHost} = state.get().options
14
+ const useProjectHostname = !!instance.config.studioMode?.enabled
15
+ const projectId = instance.config.projectId
13
16
 
14
17
  const currentUser$ = state.observable
15
18
  .pipe(
16
- map(({authState}) => authState),
19
+ map(({authState, options}) => ({authState, authMethod: options.authMethod})),
17
20
  filter(
18
- (authState): authState is Extract<AuthState, {type: AuthStateType.LOGGED_IN}> =>
19
- authState.type === AuthStateType.LOGGED_IN && !authState.currentUser,
21
+ (
22
+ value,
23
+ ): value is {
24
+ authState: Extract<AuthState, {type: AuthStateType.LOGGED_IN}>
25
+ authMethod: AuthMethodOptions
26
+ } => value.authState.type === AuthStateType.LOGGED_IN && !value.authState.currentUser,
27
+ ),
28
+ map((value) => ({token: value.authState.token, authMethod: value.authMethod})),
29
+ distinctUntilChanged(
30
+ (prev, curr) => prev.token === curr.token && prev.authMethod === curr.authMethod,
20
31
  ),
21
- map((authState) => authState.token),
22
- distinctUntilChanged(),
23
32
  )
24
33
  .pipe(
25
- map((token) =>
34
+ map(({token, authMethod}) =>
26
35
  clientFactory({
27
36
  apiVersion: DEFAULT_API_VERSION,
28
37
  requestTagPrefix: REQUEST_TAG_PREFIX,
29
- token,
38
+ token: authMethod === 'cookie' ? undefined : token,
30
39
  ignoreBrowserTokenWarning: true,
31
- useProjectHostname: false,
40
+ useProjectHostname,
32
41
  useCdn: false,
42
+ ...(authMethod === 'cookie' ? {withCredentials: true} : {}),
43
+ ...(useProjectHostname && projectId ? {projectId} : {}),
33
44
  ...(apiHost && {apiHost}),
34
45
  }),
35
46
  ),
@@ -4,6 +4,7 @@ import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
4
4
  import {AUTH_CODE_PARAM, DEFAULT_BASE} from './authConstants'
5
5
  import {
6
6
  getAuthCode,
7
+ getCleanedUrl,
7
8
  getDefaultLocation,
8
9
  getDefaultStorage,
9
10
  getStorageEvents,
@@ -230,3 +231,36 @@ describe('getTokenFromLocation', () => {
230
231
  expect(result).toBe(testToken)
231
232
  })
232
233
  })
234
+
235
+ describe('getCleanedUrl', () => {
236
+ it('removes only token from hash when it is the only param', () => {
237
+ const url = 'http://example.com/page#token=abc'
238
+ const cleaned = getCleanedUrl(url)
239
+ expect(cleaned).toBe('http://example.com/page')
240
+ })
241
+
242
+ it('removes only token from hash and preserves other hash params', () => {
243
+ const url = 'http://example.com/page#token=abc&foo=bar'
244
+ const cleaned = getCleanedUrl(url)
245
+ expect(cleaned).toBe('http://example.com/page#foo=bar')
246
+ })
247
+
248
+ it('removes token when it appears among multiple hash params', () => {
249
+ const url = 'http://example.com/page#foo=bar&token=abc&baz=qux'
250
+ const cleaned = getCleanedUrl(url)
251
+ expect(cleaned).toBe('http://example.com/page#foo=bar&baz=qux')
252
+ })
253
+
254
+ it('removes sid and url from query string while preserving others', () => {
255
+ const url =
256
+ 'http://example.com/callback?sid=s1&url=https%3A%2F%2Freturn.example%2Fdone&x=1#token=abc'
257
+ const cleaned = getCleanedUrl(url)
258
+ expect(cleaned).toBe('http://example.com/callback?x=1')
259
+ })
260
+
261
+ it('preserves non key-value hash fragments', () => {
262
+ const url = 'http://example.com/page#section'
263
+ const cleaned = getCleanedUrl(url)
264
+ expect(cleaned).toBe('http://example.com/page#section')
265
+ })
266
+ })
package/src/auth/utils.ts CHANGED
@@ -116,12 +116,20 @@ export function getDefaultLocation(): string {
116
116
  }
117
117
 
118
118
  /**
119
- * Cleans up the URL by removing the hash and the sid and url parameters.
119
+ * Cleans up the URL by removing the `token` from the hash and the `sid` and `url` search params.
120
120
  * @internal
121
121
  */
122
122
  export function getCleanedUrl(locationUrl: string): string {
123
123
  const loc = new URL(locationUrl)
124
- loc.hash = ''
124
+ // Remove only the `token` param from the hash while preserving other fragments
125
+ const rawHash = loc.hash.startsWith('#') ? loc.hash.slice(1) : loc.hash
126
+ if (rawHash && rawHash.includes('=')) {
127
+ const hashParams = new URLSearchParams(rawHash)
128
+ hashParams.delete('token')
129
+ hashParams.delete('withSid')
130
+ const nextHash = hashParams.toString()
131
+ loc.hash = nextHash ? `#${nextHash}` : ''
132
+ }
125
133
  loc.searchParams.delete('sid')
126
134
  loc.searchParams.delete('url')
127
135
  return loc.toString()
@@ -1,10 +1,9 @@
1
1
  import {type SanityDocument} from '@sanity/types'
2
- import {type ExprNode} from 'groq-js'
2
+ import {evaluateSync, type ExprNode, parse} from 'groq-js'
3
3
  import {describe, expect, it} from 'vitest'
4
4
 
5
5
  import {createSanityInstance} from '../store/createSanityInstance'
6
6
  import {getDraftId, getPublishedId} from '../utils/ids'
7
- import {evaluateSync, parse} from './_synchronous-groq-js.mjs'
8
7
  import {type DocumentAction} from './actions'
9
8
  import {calculatePermissions, createGrantsLookup, type DatasetAcl, type Grant} from './permissions'
10
9
  import {type SyncTransactionState} from './reducers'
@@ -50,7 +49,7 @@ describe('createGrantsLookup', () => {
50
49
  ;(['read', 'update', 'create', 'history'] as Grant[]).forEach((key) => {
51
50
  expect(grants[key]).toBeDefined()
52
51
  // Evaluate the expression for the dummy document.
53
- expect(evaluateSync(grants[key], {params: {document: dummyDoc}}).get()).toBe(true)
52
+ expect(evaluateSync(grants[key], {params: {document: dummyDoc}}).data).toBe(true)
54
53
  })
55
54
  })
56
55
  })
@@ -1,11 +1,10 @@
1
1
  import {type SanityDocument} from '@sanity/types'
2
- import {type ExprNode} from 'groq-js'
2
+ import {evaluateSync, type ExprNode, parse} from 'groq-js'
3
3
  import {createSelector} from 'reselect'
4
4
 
5
5
  import {type SelectorContext} from '../store/createStateSourceAction'
6
6
  import {getDraftId, getPublishedId} from '../utils/ids'
7
7
  import {MultiKeyWeakMap} from '../utils/MultiKeyWeakMap'
8
- import {evaluateSync, parse} from './_synchronous-groq-js.mjs'
9
8
  import {type DocumentAction} from './actions'
10
9
  import {ActionError, PermissionActionError, processActions} from './processActions'
11
10
  import {type DocumentSet} from './processMutations'
@@ -127,7 +126,8 @@ const memoizedActionsSelector = createSelector(
127
126
  )
128
127
 
129
128
  function checkGrant(grantExpr: ExprNode, document: SanityDocument): boolean {
130
- return evaluateSync(grantExpr, {params: {document}}).get()
129
+ const value = evaluateSync(grantExpr, {params: {document}})
130
+ return value.type === 'boolean' && value.data
131
131
  }
132
132
 
133
133
  /** @beta */
@@ -1,7 +1,7 @@
1
1
  import {type Reference, type SanityDocument} from '@sanity/types'
2
+ import {parse} from 'groq-js'
2
3
  import {describe, expect, it} from 'vitest'
3
4
 
4
- import {parse} from './_synchronous-groq-js.mjs'
5
5
  import {type DocumentAction} from './actions'
6
6
  import {ActionError, processActions} from './processActions'
7
7
  import {type DocumentSet} from './processMutations'
@@ -5,18 +5,18 @@ import {
5
5
  type Reference,
6
6
  type SanityDocument,
7
7
  } from '@sanity/types'
8
- import {type ExprNode} from 'groq-js'
8
+ import {evaluateSync, type ExprNode} from 'groq-js'
9
9
  import {isEqual} from 'lodash-es'
10
10
 
11
11
  import {getDraftId, getPublishedId} from '../utils/ids'
12
- import {evaluateSync} from './_synchronous-groq-js.mjs'
13
12
  import {type DocumentAction} from './actions'
14
13
  import {type Grant} from './permissions'
15
14
  import {type DocumentSet, getId, processMutations} from './processMutations'
16
15
  import {type HttpAction} from './reducers'
17
16
 
18
17
  function checkGrant(grantExpr: ExprNode, document: SanityDocument): boolean {
19
- return evaluateSync(grantExpr, {params: {document}}).get()
18
+ const value = evaluateSync(grantExpr, {params: {document}})
19
+ return value.type === 'boolean' && value.data
20
20
  }
21
21
 
22
22
  interface ProcessActionsOptions {
@@ -12,11 +12,11 @@ import {
12
12
  import {hashString} from '../utils/hashString'
13
13
  import {getPublishedId, insecureRandomId} from '../utils/ids'
14
14
  import {projectionStore} from './projectionStore'
15
- import {type ProjectionStoreState, type ProjectionValuePending, type ValidProjection} from './types'
15
+ import {type ProjectionStoreState, type ProjectionValuePending} from './types'
16
16
  import {PROJECTION_STATE_CLEAR_DELAY, STABLE_EMPTY_PROJECTION, validateProjection} from './util'
17
17
 
18
18
  export interface ProjectionOptions<
19
- TProjection extends ValidProjection = ValidProjection,
19
+ TProjection extends string = string,
20
20
  TDocumentType extends string = string,
21
21
  TDataset extends string = string,
22
22
  TProjectId extends string = string,
@@ -28,7 +28,7 @@ export interface ProjectionOptions<
28
28
  * @beta
29
29
  */
30
30
  export function getProjectionState<
31
- TProjection extends ValidProjection = ValidProjection,
31
+ TProjection extends string = string,
32
32
  TDocumentType extends string = string,
33
33
  TDataset extends string = string,
34
34
  TProjectId extends string = string,
@@ -75,13 +75,13 @@ export const _getProjectionState = bindActionByDataset(
75
75
  createStateSourceAction({
76
76
  selector: (
77
77
  {state}: SelectorContext<ProjectionStoreState>,
78
- options: ProjectionOptions<ValidProjection, string, string, string>,
78
+ options: ProjectionOptions<string, string, string, string>,
79
79
  ): ProjectionValuePending<object> | undefined => {
80
80
  const documentId = getPublishedId(options.documentId)
81
81
  const projectionHash = hashString(options.projection)
82
82
  return state.values[documentId]?.[projectionHash] ?? STABLE_EMPTY_PROJECTION
83
83
  },
84
- onSubscribe: ({state}, options: ProjectionOptions<ValidProjection, string, string, string>) => {
84
+ onSubscribe: ({state}, options: ProjectionOptions<string, string, string, string>) => {
85
85
  const {projection, ...docHandle} = options
86
86
  const subscriptionId = insecureRandomId()
87
87
  const documentId = getPublishedId(docHandle.documentId)
@@ -1,12 +1,11 @@
1
1
  import {describe, expect, it} from 'vitest'
2
2
 
3
3
  import {createProjectionQuery, processProjectionQuery} from './projectionQuery'
4
- import {type ValidProjection} from './types'
5
4
 
6
5
  describe('createProjectionQuery', () => {
7
6
  it('creates a query and params for given ids and projections', () => {
8
7
  const ids = new Set(['doc1', 'doc2'])
9
- const projectionHash: ValidProjection = '{title, description}'
8
+ const projectionHash = '{title, description}'
10
9
  const documentProjections = {
11
10
  doc1: {[projectionHash]: projectionHash},
12
11
  doc2: {[projectionHash]: projectionHash},
@@ -21,8 +20,8 @@ describe('createProjectionQuery', () => {
21
20
 
22
21
  it('handles multiple different projections', () => {
23
22
  const ids = new Set(['doc1', 'doc2'])
24
- const projectionHash1: ValidProjection = '{title, description}'
25
- const projectionHash2: ValidProjection = '{name, age}'
23
+ const projectionHash1 = '{title, description}'
24
+ const projectionHash2 = '{name, age}'
26
25
  const documentProjections = {
27
26
  doc1: {[projectionHash1]: projectionHash1},
28
27
  doc2: {[projectionHash2]: projectionHash2},
@@ -39,9 +38,9 @@ describe('createProjectionQuery', () => {
39
38
 
40
39
  it('filters out ids without projections', () => {
41
40
  const ids = new Set(['doc1', 'doc2', 'doc3'])
42
- const projectionHash1: ValidProjection = '{title}'
41
+ const projectionHash1 = '{title}'
43
42
  // projectionHash2 missing intentionally
44
- const projectionHash3: ValidProjection = '{name}'
43
+ const projectionHash3 = '{name}'
45
44
 
46
45
  const documentProjections = {
47
46
  doc1: {[projectionHash1]: projectionHash1},
@@ -1,9 +1,6 @@
1
1
  import {getDraftId, getPublishedId} from '../utils/ids'
2
- import {
3
- type DocumentProjections,
4
- type DocumentProjectionValues,
5
- type ValidProjection,
6
- } from './types'
2
+ import {type DocumentProjections, type DocumentProjectionValues} from './types'
3
+ import {validateProjection} from './util'
7
4
 
8
5
  export type ProjectionQueryResult = {
9
6
  _id: string
@@ -18,7 +15,7 @@ interface CreateProjectionQueryResult {
18
15
  params: Record<string, unknown>
19
16
  }
20
17
 
21
- type ProjectionMap = Record<string, {projection: ValidProjection; documentIds: Set<string>}>
18
+ type ProjectionMap = Record<string, {projection: string; documentIds: Set<string>}>
22
19
 
23
20
  export function createProjectionQuery(
24
21
  documentIds: Set<string>,
@@ -31,7 +28,7 @@ export function createProjectionQuery(
31
28
 
32
29
  return Object.entries(projectionsForDoc).map(([projectionHash, projection]) => ({
33
30
  documentId: id,
34
- projection,
31
+ projection: validateProjection(projection),
35
32
  projectionHash,
36
33
  }))
37
34
  })
@@ -6,7 +6,7 @@ import {createSanityInstance, type SanityInstance} from '../store/createSanityIn
6
6
  import {type StateSource} from '../store/createStateSourceAction'
7
7
  import {getProjectionState} from './getProjectionState'
8
8
  import {resolveProjection} from './resolveProjection'
9
- import {type ProjectionValuePending, type ValidProjection} from './types'
9
+ import {type ProjectionValuePending} from './types'
10
10
 
11
11
  vi.mock('./getProjectionState')
12
12
 
@@ -35,7 +35,7 @@ describe('resolveProjection', () => {
35
35
  documentId: 'doc123',
36
36
  documentType: 'movie',
37
37
  })
38
- const projection = '{title}' as ValidProjection
38
+ const projection = '{title}'
39
39
 
40
40
  const result = await resolveProjection(instance, {...docHandle, projection})
41
41
 
@@ -5,11 +5,11 @@ import {bindActionByDataset} from '../store/createActionBinder'
5
5
  import {type SanityInstance} from '../store/createSanityInstance'
6
6
  import {getProjectionState, type ProjectionOptions} from './getProjectionState'
7
7
  import {projectionStore} from './projectionStore'
8
- import {type ProjectionValuePending, type ValidProjection} from './types'
8
+ import {type ProjectionValuePending} from './types'
9
9
 
10
10
  /** @beta */
11
11
  export function resolveProjection<
12
- TProjection extends ValidProjection = ValidProjection,
12
+ TProjection extends string = string,
13
13
  TDocumentType extends string = string,
14
14
  TDataset extends string = string,
15
15
  TProjectId extends string = string,