@sanity/sdk-react 2.18.0 → 3.0.0-rc.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.
Files changed (54) hide show
  1. package/dist/index.d.ts +267 -112
  2. package/dist/index.js +131 -89
  3. package/dist/index.js.map +1 -1
  4. package/package.json +11 -9
  5. package/src/_exports/sdk-react.ts +14 -1
  6. package/src/components/auth/AuthBoundary.tsx +10 -5
  7. package/src/context/WorkbenchTokenRefresh.test.tsx +106 -0
  8. package/src/context/WorkbenchTokenRefresh.tsx +61 -0
  9. package/src/context/workbenchToken.ts +63 -0
  10. package/src/hooks/access/useCheckPermissions.test-d.ts +15 -0
  11. package/src/hooks/access/useCheckPermissions.test.tsx +53 -0
  12. package/src/hooks/access/useCheckPermissions.ts +24 -0
  13. package/src/hooks/applications/useApplication.test-d.ts +30 -0
  14. package/src/hooks/applications/useApplication.ts +22 -0
  15. package/src/hooks/applications/useApplications.test-d.ts +31 -0
  16. package/src/hooks/applications/useApplications.ts +25 -0
  17. package/src/hooks/applications/useCreateUserApplication.test-d.ts +14 -0
  18. package/src/hooks/applications/useCreateUserApplication.ts +11 -0
  19. package/src/hooks/applications/useDeleteApplication.test-d.ts +12 -0
  20. package/src/hooks/applications/useDeleteApplication.ts +11 -0
  21. package/src/hooks/applications/useDeleteUserApplication.test-d.ts +14 -0
  22. package/src/hooks/applications/useDeleteUserApplication.ts +11 -0
  23. package/src/hooks/applications/useUpdateApplication.test-d.ts +12 -0
  24. package/src/hooks/applications/useUpdateApplication.ts +11 -0
  25. package/src/hooks/applications/useUpdateUserApplication.test-d.ts +14 -0
  26. package/src/hooks/applications/useUpdateUserApplication.ts +11 -0
  27. package/src/hooks/applications/useUserApplication.test-d.ts +11 -0
  28. package/src/hooks/applications/useUserApplication.ts +14 -0
  29. package/src/hooks/applications/useUserApplications.test-d.ts +11 -0
  30. package/src/hooks/applications/useUserApplications.ts +14 -0
  31. package/src/hooks/helpers/createFetcherHook.test.tsx +180 -0
  32. package/src/hooks/helpers/createFetcherHook.ts +69 -0
  33. package/src/hooks/helpers/createMutationHook.test.tsx +125 -0
  34. package/src/hooks/helpers/createMutationHook.tsx +93 -0
  35. package/src/hooks/installations/useInstallation.test-d.ts +19 -0
  36. package/src/hooks/installations/useInstallation.ts +22 -0
  37. package/src/hooks/installations/useInstallations.test-d.ts +26 -0
  38. package/src/hooks/installations/useInstallations.ts +25 -0
  39. package/src/hooks/organizations/useOrganization.test-d.ts +19 -12
  40. package/src/hooks/organizations/useOrganization.test.ts +50 -52
  41. package/src/hooks/organizations/useOrganization.ts +13 -19
  42. package/src/hooks/organizations/useOrganizations.test-d.ts +26 -13
  43. package/src/hooks/organizations/useOrganizations.test.ts +49 -71
  44. package/src/hooks/organizations/useOrganizations.ts +14 -20
  45. package/src/hooks/projects/useProject.test-d.ts +18 -11
  46. package/src/hooks/projects/useProject.test.tsx +29 -23
  47. package/src/hooks/projects/useProject.ts +12 -16
  48. package/src/hooks/projects/useProjects.test-d.ts +22 -11
  49. package/src/hooks/projects/useProjects.test.ts +45 -98
  50. package/src/hooks/projects/useProjects.ts +16 -17
  51. package/src/hooks/dashboard/useDispatchIntent.test.ts +0 -251
  52. package/src/hooks/dashboard/useDispatchIntent.ts +0 -157
  53. package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.test.ts +0 -128
  54. package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.ts +0 -42
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/sdk-react",
3
- "version": "2.18.0",
3
+ "version": "3.0.0-rc.1",
4
4
  "private": false,
5
5
  "description": "Sanity SDK React toolkit for Content OS",
6
6
  "keywords": [
@@ -44,19 +44,21 @@
44
44
  "access": "public"
45
45
  },
46
46
  "dependencies": {
47
- "@sanity/client": "^7.23.1",
47
+ "@sanity/client": "^7.23.2",
48
48
  "@sanity/message-protocol": "^0.23.0",
49
- "@sanity/types": "^6.2.0",
49
+ "@sanity/types": "^6.5.0",
50
+ "@sanity/workbench": "0.1.0-alpha.24",
50
51
  "groq": "3.88.1-typegen-experimental.0",
51
52
  "react-compiler-runtime": "^1.0.0",
52
53
  "react-error-boundary": "^6.1.2",
53
54
  "rxjs": "^7.8.2",
54
- "@sanity/sdk": "2.18.0"
55
+ "xstate": "^5.31.0",
56
+ "@sanity/sdk": "3.0.0-rc.1"
55
57
  },
56
58
  "devDependencies": {
57
59
  "@sanity/browserslist-config": "^1.0.5",
58
60
  "@sanity/comlink": "^4.0.1",
59
- "@sanity/pkg-utils": "^10.5.8",
61
+ "@sanity/pkg-utils": "^10.9.2",
60
62
  "@testing-library/jest-dom": "^6.9.1",
61
63
  "@testing-library/react": "^16.3.2",
62
64
  "@types/node": "^24.13.3",
@@ -66,20 +68,20 @@
66
68
  "@vitest/coverage-v8": "^4.1.10",
67
69
  "babel-plugin-react-compiler": "^1.0.0",
68
70
  "eslint": "^10.6.0",
69
- "groq-js": "^1.30.3",
71
+ "groq-js": "^2.0.0",
70
72
  "jsdom": "^29.1.1",
71
73
  "oxfmt": "^0.58.0",
72
74
  "react": "^19.2.7",
73
75
  "react-dom": "^19.2.7",
74
76
  "rollup-plugin-visualizer": "^7.0.1",
75
77
  "typescript": "^6.0.3",
76
- "vite": "^8.1.4",
78
+ "vite": "^8.1.5",
77
79
  "vitest": "^4.1.10",
78
80
  "@repo/config-eslint": "0.0.0",
79
81
  "@repo/config-test": "0.0.1",
80
82
  "@repo/package.bundle": "3.82.0",
81
- "@repo/tsconfig": "0.0.1",
82
- "@repo/package.config": "0.0.1"
83
+ "@repo/package.config": "0.0.1",
84
+ "@repo/tsconfig": "0.0.1"
83
85
  },
84
86
  "peerDependencies": {
85
87
  "react": "^18.0.0 || ^19.0.0",
@@ -13,6 +13,7 @@ export {
13
13
  type SanityInstanceProviderProps,
14
14
  } from '../context/SanityInstanceProvider'
15
15
  export {SDKStudioContext, type StudioWorkspaceHandle} from '../context/SDKStudioContext'
16
+ export {useCheckPermissions} from '../hooks/access/useCheckPermissions'
16
17
  export {
17
18
  useAgentGenerate,
18
19
  useAgentPatch,
@@ -24,6 +25,15 @@ export {
24
25
  type AgentResourceContextOptions,
25
26
  useAgentResourceContext,
26
27
  } from '../hooks/agent/useAgentResourceContext'
28
+ export {useApplication} from '../hooks/applications/useApplication'
29
+ export {useApplications} from '../hooks/applications/useApplications'
30
+ export {useCreateUserApplication} from '../hooks/applications/useCreateUserApplication'
31
+ export {useDeleteApplication} from '../hooks/applications/useDeleteApplication'
32
+ export {useDeleteUserApplication} from '../hooks/applications/useDeleteUserApplication'
33
+ export {useUpdateApplication} from '../hooks/applications/useUpdateApplication'
34
+ export {useUpdateUserApplication} from '../hooks/applications/useUpdateUserApplication'
35
+ export {useUserApplication} from '../hooks/applications/useUserApplication'
36
+ export {useUserApplications} from '../hooks/applications/useUserApplications'
27
37
  export {useAuthState} from '../hooks/auth/useAuthState'
28
38
  export {useAuthToken} from '../hooks/auth/useAuthToken'
29
39
  export {useCurrentUser} from '../hooks/auth/useCurrentUser'
@@ -48,7 +58,6 @@ export {
48
58
  export {useResource} from '../hooks/context/useResource'
49
59
  export {useSanityInstance} from '../hooks/context/useSanityInstance'
50
60
  export {useDashboardNavigate} from '../hooks/dashboard/useDashboardNavigate'
51
- export {useDispatchIntent} from '../hooks/dashboard/useDispatchIntent'
52
61
  export {useManageFavorite} from '../hooks/dashboard/useManageFavorite'
53
62
  export {
54
63
  type NavigateToStudioResult,
@@ -70,6 +79,10 @@ export {
70
79
  type DocumentsResponse,
71
80
  useDocuments,
72
81
  } from '../hooks/documents/useDocuments'
82
+ export {type FetcherHookResult} from '../hooks/helpers/createFetcherHook'
83
+ export {type MutationHookResult} from '../hooks/helpers/createMutationHook'
84
+ export {useInstallation} from '../hooks/installations/useInstallation'
85
+ export {useInstallations} from '../hooks/installations/useInstallations'
73
86
  export {useOrganization} from '../hooks/organizations/useOrganization'
74
87
  export {useOrganizations} from '../hooks/organizations/useOrganizations'
75
88
  export {
@@ -4,6 +4,8 @@ import {useEffect, useMemo} from 'react'
4
4
  import {ErrorBoundary, type FallbackProps} from 'react-error-boundary'
5
5
 
6
6
  import {ComlinkTokenRefreshProvider} from '../../context/ComlinkTokenRefresh'
7
+ import {isWorkbenchEnvironment} from '../../context/workbenchToken'
8
+ import {WorkbenchTokenRefreshProvider} from '../../context/WorkbenchTokenRefresh'
7
9
  import {useAuthState} from '../../hooks/auth/useAuthState'
8
10
  import {useLoginUrl} from '../../hooks/auth/useLoginUrl'
9
11
  import {useVerifyOrgProjects} from '../../hooks/auth/useVerifyOrgProjects'
@@ -141,9 +143,11 @@ export function AuthBoundary({
141
143
 
142
144
  return (
143
145
  <ComlinkTokenRefreshProvider>
144
- <ErrorBoundary FallbackComponent={FallbackComponent} resetKeys={[sessionResetKey]}>
145
- <AuthSwitch {...props} />
146
- </ErrorBoundary>
146
+ <WorkbenchTokenRefreshProvider>
147
+ <ErrorBoundary FallbackComponent={FallbackComponent} resetKeys={[sessionResetKey]}>
148
+ <AuthSwitch {...props} />
149
+ </ErrorBoundary>
150
+ </WorkbenchTokenRefreshProvider>
147
151
  </ComlinkTokenRefreshProvider>
148
152
  )
149
153
  }
@@ -182,8 +186,9 @@ function AuthSwitch({
182
186
  const loginUrl = useLoginUrl()
183
187
 
184
188
  useEffect(() => {
185
- if (isLoggedOut && !isInIframe() && !isStudio) {
186
- // We don't want to redirect to login if we're in the Dashboard nor in studio mode
189
+ if (isLoggedOut && !isInIframe() && !isStudio && !isWorkbenchEnvironment()) {
190
+ // We don't want to redirect to login if we're in the Dashboard, in studio
191
+ // mode, or in the workbench (the OS owns the session and mints the token)
187
192
  window.location.href = loginUrl
188
193
  }
189
194
  }, [isLoggedOut, loginUrl, isStudio])
@@ -0,0 +1,106 @@
1
+ import {AuthStateType, setAuthToken} from '@sanity/sdk'
2
+ import {act, render} from '@testing-library/react'
3
+ import {of} from 'rxjs'
4
+ import {afterEach, beforeEach, describe, expect, it, type Mock, vi} from 'vitest'
5
+
6
+ import {useAuthState} from '../hooks/auth/useAuthState'
7
+ import {ResourceProvider} from './ResourceProvider'
8
+ import {
9
+ isWorkbenchEnvironment,
10
+ observeWorkbenchToken,
11
+ refreshWorkbenchToken,
12
+ } from './workbenchToken'
13
+ import {WorkbenchTokenRefreshProvider} from './WorkbenchTokenRefresh'
14
+
15
+ vi.mock('@sanity/sdk', async () => {
16
+ const actual = await vi.importActual('@sanity/sdk')
17
+ return {
18
+ ...actual,
19
+ setAuthToken: vi.fn(),
20
+ }
21
+ })
22
+
23
+ vi.mock('../hooks/auth/useAuthState', () => ({
24
+ useAuthState: vi.fn(),
25
+ }))
26
+
27
+ vi.mock('./workbenchToken', () => ({
28
+ isWorkbenchEnvironment: vi.fn(() => false),
29
+ observeWorkbenchToken: vi.fn(() => undefined),
30
+ refreshWorkbenchToken: vi.fn(),
31
+ }))
32
+
33
+ const mockSetAuthToken = setAuthToken as Mock
34
+ const mockUseAuthState = useAuthState as Mock
35
+ const mockIsWorkbenchEnvironment = isWorkbenchEnvironment as Mock
36
+ const mockObserveWorkbenchToken = observeWorkbenchToken as Mock
37
+ const mockRefreshWorkbenchToken = refreshWorkbenchToken as Mock
38
+
39
+ const renderProvider = () =>
40
+ render(
41
+ <ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
42
+ <WorkbenchTokenRefreshProvider>
43
+ <div>Test</div>
44
+ </WorkbenchTokenRefreshProvider>
45
+ </ResourceProvider>,
46
+ )
47
+
48
+ describe('WorkbenchTokenRefreshProvider', () => {
49
+ beforeEach(() => {
50
+ mockUseAuthState.mockReturnValue({type: AuthStateType.LOGGED_IN})
51
+ })
52
+
53
+ afterEach(() => {
54
+ vi.clearAllMocks()
55
+ })
56
+
57
+ describe('when not in the workbench', () => {
58
+ it('does not subscribe to the OS token', () => {
59
+ mockIsWorkbenchEnvironment.mockReturnValue(false)
60
+
61
+ act(() => {
62
+ renderProvider()
63
+ })
64
+
65
+ expect(mockSetAuthToken).not.toHaveBeenCalled()
66
+ })
67
+ })
68
+
69
+ describe('when in the workbench', () => {
70
+ beforeEach(() => {
71
+ mockIsWorkbenchEnvironment.mockReturnValue(true)
72
+ })
73
+
74
+ it('mirrors the OS token into the auth store', () => {
75
+ mockObserveWorkbenchToken.mockReturnValue(of('workbench-token'))
76
+
77
+ act(() => {
78
+ renderProvider()
79
+ })
80
+
81
+ expect(mockSetAuthToken).toHaveBeenCalledWith(expect.anything(), 'workbench-token')
82
+ })
83
+
84
+ it('asks the OS to reissue the token on a 401', () => {
85
+ mockObserveWorkbenchToken.mockReturnValue(of('workbench-token'))
86
+
87
+ const {rerender} = renderProvider()
88
+
89
+ mockUseAuthState.mockReturnValue({
90
+ type: AuthStateType.ERROR,
91
+ error: {statusCode: 401, message: 'Unauthorized'},
92
+ })
93
+ act(() => {
94
+ rerender(
95
+ <ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
96
+ <WorkbenchTokenRefreshProvider>
97
+ <div>Test</div>
98
+ </WorkbenchTokenRefreshProvider>
99
+ </ResourceProvider>,
100
+ )
101
+ })
102
+
103
+ expect(mockRefreshWorkbenchToken).toHaveBeenCalledTimes(1)
104
+ })
105
+ })
106
+ })
@@ -0,0 +1,61 @@
1
+ import {type ClientError} from '@sanity/client'
2
+ import {AuthStateType, setAuthToken} from '@sanity/sdk'
3
+ import React, {type PropsWithChildren, useEffect, useRef} from 'react'
4
+
5
+ import {useAuthState} from '../hooks/auth/useAuthState'
6
+ import {useSanityInstance} from '../hooks/context/useSanityInstance'
7
+ import {
8
+ isWorkbenchEnvironment,
9
+ observeWorkbenchToken,
10
+ refreshWorkbenchToken,
11
+ } from './workbenchToken'
12
+
13
+ /**
14
+ * Keeps the SDK auth token in sync with the workbench "OS".
15
+ *
16
+ * When running as a federated remote inside the workbench the OS owns the
17
+ * session, so we subscribe to its `auth.token` stream and mirror each value into
18
+ * the auth store — a token logs us in, `null` logs us out, and later OS
19
+ * sign-in/out propagates automatically. When a request is rejected with a 401
20
+ * (the token expired), we ask the OS to reissue rather than tearing the session
21
+ * down; the new token arrives back through the same subscription.
22
+ */
23
+ function WorkbenchTokenRefresh({children}: PropsWithChildren) {
24
+ const instance = useSanityInstance()
25
+ const authState = useAuthState()
26
+ const processed401ErrorRef = useRef<unknown | null>(null)
27
+
28
+ useEffect(() => {
29
+ const token$ = observeWorkbenchToken()
30
+ if (!token$) return undefined
31
+ const subscription = token$.subscribe((token) => setAuthToken(instance, token))
32
+ return () => subscription.unsubscribe()
33
+ }, [instance])
34
+
35
+ useEffect(() => {
36
+ const has401Error =
37
+ authState.type === AuthStateType.ERROR && (authState.error as ClientError)?.statusCode === 401
38
+
39
+ if (has401Error && processed401ErrorRef.current !== authState.error) {
40
+ processed401ErrorRef.current = authState.error
41
+ refreshWorkbenchToken()
42
+ } else if (!has401Error) {
43
+ processed401ErrorRef.current = null
44
+ }
45
+ }, [authState])
46
+
47
+ return children
48
+ }
49
+
50
+ /**
51
+ * Provides workbench OS token refresh. No-op outside the workbench, where the
52
+ * app uses its normal auth flow.
53
+ * @public
54
+ */
55
+ export const WorkbenchTokenRefreshProvider: React.FC<PropsWithChildren> = ({children}) => {
56
+ if (isWorkbenchEnvironment()) {
57
+ return <WorkbenchTokenRefresh>{children}</WorkbenchTokenRefresh>
58
+ }
59
+
60
+ return children
61
+ }
@@ -0,0 +1,63 @@
1
+ import {from, type Observable, of} from 'rxjs'
2
+ import {catchError, switchMap} from 'rxjs/operators'
3
+
4
+ // The workbench host installs its shared message bus on this well-known global
5
+ // symbol before it loads federated remotes. It must match the key used by
6
+ // `@sanity/workbench` (`Symbol.for('sanity.os.bus')`).
7
+ const OS_BUS_KEY = Symbol.for('sanity.os.bus')
8
+
9
+ /**
10
+ * Whether this app is running as a federated remote inside the workbench.
11
+ *
12
+ * Federation shares the host's realm, so the installed bus is visible on
13
+ * `globalThis`. This is `false` in a standalone app, where we must never import
14
+ * `@sanity/workbench` (it would install a bus and add bundle weight for no
15
+ * reason). Note: this is a different embedding model to the Core UI iframe,
16
+ * which is detected separately via the dashboard context — that signal is not
17
+ * set on the federation path.
18
+ *
19
+ * @internal
20
+ */
21
+ export function isWorkbenchEnvironment(): boolean {
22
+ return typeof globalThis === 'object' && OS_BUS_KEY in globalThis
23
+ }
24
+
25
+ /**
26
+ * Observes the session token issued by the workbench "OS", tracking the OS auth
27
+ * state over time.
28
+ *
29
+ * Returns `undefined` when the app is not embedded in the workbench, so the
30
+ * caller uses its normal auth flow. Inside the workbench, subscribes to the
31
+ * `auth.token` state topic, emitting the current token — or `null` when the OS
32
+ * is signed out — and re-emitting as the OS auth state changes, so sign-in/out
33
+ * propagates instead of being captured once. Any bus error is treated as "no
34
+ * token" (`null`). The token is used in-memory only and never persisted.
35
+ *
36
+ * @internal
37
+ */
38
+ export function observeWorkbenchToken(): Observable<string | null> | undefined {
39
+ if (!isWorkbenchEnvironment()) return undefined
40
+
41
+ return from(import('@sanity/workbench')).pipe(
42
+ switchMap(({os}) => os.subscribe('auth.token')),
43
+ // Any failure (importing the host bundle, or the subscription) means "no OS token".
44
+ catchError(() => of(null)),
45
+ )
46
+ }
47
+
48
+ /**
49
+ * Asks the workbench "OS" to reissue the session token, e.g. after its current
50
+ * one was rejected with a 401. Fire-and-forget: the reissued token arrives via
51
+ * the `auth.token` subscription in {@link observeWorkbenchToken}. No-op outside
52
+ * the workbench.
53
+ *
54
+ * @internal
55
+ */
56
+ export function refreshWorkbenchToken(): void {
57
+ if (!isWorkbenchEnvironment()) return
58
+
59
+ void import('@sanity/workbench').then(
60
+ ({os}) => os.emit('auth.token.refresh', undefined),
61
+ () => {},
62
+ )
63
+ }
@@ -0,0 +1,15 @@
1
+ import {expectTypeOf, test} from 'vitest'
2
+
3
+ import {type FetcherHookResult} from '../helpers/createFetcherHook'
4
+ import {useCheckPermissions} from './useCheckPermissions'
5
+
6
+ test('useCheckPermissions — the result is keyed by exactly the permissions passed', () => {
7
+ const result = useCheckPermissions('project', 'proj_1', ['read', 'write'])
8
+ expectTypeOf(result).toEqualTypeOf<FetcherHookResult<Record<'read' | 'write', boolean>>>()
9
+ expectTypeOf(result.data).toEqualTypeOf<Record<'read' | 'write', boolean>>()
10
+ })
11
+
12
+ test('useCheckPermissions — rejects an unknown resource type', () => {
13
+ // @ts-expect-error — resourceType must be an AccessResourceType
14
+ void useCheckPermissions('not-a-resource', 'id', ['read'])
15
+ })
@@ -0,0 +1,53 @@
1
+ import {checkPermissions, type StateSource} from '@sanity/sdk'
2
+ import {type FetcherSnapshot} from '@sanity/sdk/_internal'
3
+ import {type Observable} from 'rxjs'
4
+ import {beforeEach, describe, expect, it, vi} from 'vitest'
5
+
6
+ import {renderHook} from '../../../test/test-utils'
7
+ import {useCheckPermissions} from './useCheckPermissions'
8
+
9
+ vi.mock('@sanity/sdk', async (importOriginal) => {
10
+ const original = await importOriginal<typeof import('@sanity/sdk')>()
11
+ return {
12
+ ...original,
13
+ checkPermissions: {getState: vi.fn(), resolveState: vi.fn(), refetch: vi.fn()},
14
+ }
15
+ })
16
+
17
+ const stateSource = (
18
+ current: Record<string, boolean>,
19
+ ): StateSource<FetcherSnapshot<Record<string, boolean>>> => {
20
+ const snapshot = {
21
+ status: 'success',
22
+ data: current,
23
+ error: undefined,
24
+ isFetching: false,
25
+ dataUpdatedAt: 1,
26
+ }
27
+ return {
28
+ getCurrent: vi.fn(() => snapshot),
29
+ subscribe: vi.fn(() => () => {}),
30
+ get observable(): Observable<unknown> {
31
+ throw new Error('Not implemented')
32
+ },
33
+ } as unknown as StateSource<FetcherSnapshot<Record<string, boolean>>>
34
+ }
35
+
36
+ const sanityInstance = expect.objectContaining({config: expect.any(Object)})
37
+
38
+ describe('useCheckPermissions', () => {
39
+ beforeEach(() => {
40
+ vi.clearAllMocks()
41
+ vi.mocked(checkPermissions.getState).mockReturnValue(stateSource({'sanity.project.read': true}))
42
+ })
43
+
44
+ it('forwards every param to the fetcher and returns the permission map', () => {
45
+ const {result} = renderHook(() =>
46
+ useCheckPermissions('project', 'proj_1', ['sanity.project.read']),
47
+ )
48
+ expect(checkPermissions.getState).toHaveBeenCalledWith(sanityInstance, 'project', 'proj_1', [
49
+ 'sanity.project.read',
50
+ ])
51
+ expect(result.current.data).toEqual({'sanity.project.read': true})
52
+ })
53
+ })
@@ -0,0 +1,24 @@
1
+ import {type AccessResourceType, checkPermissions} from '@sanity/sdk'
2
+
3
+ import {createFetcherHook, type FetcherHookResult} from '../helpers/createFetcherHook'
4
+
5
+ /**
6
+ * Checks whether the current user holds the given permissions on a resource.
7
+ *
8
+ * The hook suspends until the first fetch succeeds, so `data` is always present.
9
+ * `data` maps each requested permission to whether the user has it — keyed by
10
+ * exactly the permission strings you passed.
11
+ *
12
+ * @public
13
+ * @param resourceType - The type of resource (e.g. `'project'`, `'organization'`).
14
+ * @param resourceId - The resource id.
15
+ * @param permissions - The permission names to check.
16
+ * @returns The result envelope `{data, isFetching, error, refetch}`.
17
+ */
18
+ export const useCheckPermissions = createFetcherHook(checkPermissions) as <
19
+ Permission extends string,
20
+ >(
21
+ resourceType: AccessResourceType,
22
+ resourceId: string,
23
+ permissions: Permission[],
24
+ ) => FetcherHookResult<Record<Permission, boolean>>
@@ -0,0 +1,30 @@
1
+ import {
2
+ type Application,
3
+ type ApplicationDeployment,
4
+ type ApplicationStudioConfig,
5
+ } from '@sanity/sdk'
6
+ import {expectTypeOf, test} from 'vitest'
7
+
8
+ import {type FetcherHookResult} from '../helpers/createFetcherHook'
9
+ import {useApplication} from './useApplication'
10
+
11
+ test('useApplication — no include: the base application', () => {
12
+ const result = useApplication('app_1')
13
+ expectTypeOf(result).toEqualTypeOf<FetcherHookResult<Application<never>>>()
14
+ expectTypeOf<
15
+ Extract<keyof typeof result.data, 'activeDeployment' | 'config'>
16
+ >().toEqualTypeOf<never>()
17
+ })
18
+
19
+ test('useApplication — config.studio include adds config.studio', () => {
20
+ const result = useApplication('app_1', {include: ['config.studio']})
21
+ expectTypeOf(result.data).toEqualTypeOf<Application<'config.studio'>>()
22
+ expectTypeOf(result.data.config.studio).toEqualTypeOf<ApplicationStudioConfig>()
23
+ })
24
+
25
+ test('useApplication — a deployment-child include forces the deployment in', () => {
26
+ const result = useApplication('app_1', {include: ['activeDeployment']})
27
+ expectTypeOf(
28
+ result.data.activeDeployment,
29
+ ).toEqualTypeOf<ApplicationDeployment<'activeDeployment'> | null>()
30
+ })
@@ -0,0 +1,22 @@
1
+ import {type Application, application, type ApplicationInclude} from '@sanity/sdk'
2
+
3
+ import {createFetcherHook, type FetcherHookResult} from '../helpers/createFetcherHook'
4
+
5
+ /**
6
+ * Returns a single application by id.
7
+ *
8
+ * The hook suspends until the first fetch succeeds, so `data` is always present.
9
+ * The `include` tokens you pass shape `data`: each requested token adds its
10
+ * field, and omitted ones are absent from the type.
11
+ *
12
+ * @public
13
+ * @param applicationId - The application id.
14
+ * @param options - Optional `include` list to expand related resources.
15
+ * @returns The result envelope `{data, isFetching, error, refetch}`.
16
+ */
17
+ export const useApplication = createFetcherHook(application) as <
18
+ Include extends ApplicationInclude = never,
19
+ >(
20
+ applicationId: string,
21
+ options?: {include?: Include[]},
22
+ ) => FetcherHookResult<Application<Include>>
@@ -0,0 +1,31 @@
1
+ import {type Application, type ApplicationInterface, type ApplicationsResponse} from '@sanity/sdk'
2
+ import {expectTypeOf, test} from 'vitest'
3
+
4
+ import {type FetcherHookResult} from '../helpers/createFetcherHook'
5
+ import {useApplications} from './useApplications'
6
+
7
+ test('useApplications — no include: base items, no deployment or config', () => {
8
+ const result = useApplications({organizationId: 'org_1'})
9
+ expectTypeOf(result).toEqualTypeOf<FetcherHookResult<ApplicationsResponse<never>>>()
10
+ type Item = (typeof result.data.data)[number]
11
+ expectTypeOf<Extract<keyof Item, 'activeDeployment' | 'config'>>().toEqualTypeOf<never>()
12
+ })
13
+
14
+ test('useApplications — include shapes each item', () => {
15
+ const result = useApplications({organizationId: 'org_1', include: ['interfaces']})
16
+ expectTypeOf(result.data).toEqualTypeOf<ApplicationsResponse<'interfaces'>>()
17
+ type Item = (typeof result.data.data)[number]
18
+ expectTypeOf<NonNullable<Item['activeDeployment']>['interfaces']>().toEqualTypeOf<
19
+ ApplicationInterface[]
20
+ >()
21
+ })
22
+
23
+ test('useApplications — result data is the current include-narrowed application', () => {
24
+ const result = useApplications({organizationId: 'org_1', include: ['config.studio']})
25
+ expectTypeOf(result.data.data).toEqualTypeOf<Application<'config.studio'>[]>()
26
+ })
27
+
28
+ test('useApplications — requires an organizationId', () => {
29
+ // @ts-expect-error — organizationId is required
30
+ void useApplications({})
31
+ })
@@ -0,0 +1,25 @@
1
+ import {
2
+ type ApplicationInclude,
3
+ applications,
4
+ type ApplicationsOptions,
5
+ type ApplicationsResponse,
6
+ } from '@sanity/sdk'
7
+
8
+ import {createFetcherHook, type FetcherHookResult} from '../helpers/createFetcherHook'
9
+
10
+ /**
11
+ * Returns the applications matching the given options.
12
+ *
13
+ * The hook suspends until the first fetch succeeds, so `data` is always present.
14
+ * The `include` tokens you pass shape `data.data`: each requested token adds its
15
+ * field, and omitted ones are absent from the type.
16
+ *
17
+ * @public
18
+ * @param options - Filter and include options for the applications list.
19
+ * @returns The result envelope `{data, isFetching, error, refetch}`.
20
+ */
21
+ export const useApplications = createFetcherHook(applications) as <
22
+ Include extends ApplicationInclude = never,
23
+ >(
24
+ options: ApplicationsOptions<Include>,
25
+ ) => FetcherHookResult<ApplicationsResponse<Include>>
@@ -0,0 +1,14 @@
1
+ import {type CreateUserApplicationInput, type UserApplication} from '@sanity/sdk'
2
+ import {expectTypeOf, test} from 'vitest'
3
+
4
+ import {type MutationHookResult} from '../helpers/createMutationHook'
5
+ import {useCreateUserApplication} from './useCreateUserApplication'
6
+
7
+ test('useCreateUserApplication — returns the mutation envelope', () => {
8
+ const result = useCreateUserApplication()
9
+ expectTypeOf(result).toEqualTypeOf<
10
+ MutationHookResult<CreateUserApplicationInput, UserApplication>
11
+ >()
12
+ expectTypeOf(result.mutate).parameter(0).toEqualTypeOf<CreateUserApplicationInput>()
13
+ expectTypeOf(result.data).toEqualTypeOf<UserApplication | undefined>()
14
+ })
@@ -0,0 +1,11 @@
1
+ import {createUserApplication} from '@sanity/sdk'
2
+
3
+ import {createMutationHook} from '../helpers/createMutationHook'
4
+
5
+ /**
6
+ * Creates a user application.
7
+ *
8
+ * @internal
9
+ * @returns The mutation envelope `{mutate, isPending, error, data, reset}`.
10
+ */
11
+ export const useCreateUserApplication = createMutationHook(createUserApplication)
@@ -0,0 +1,12 @@
1
+ import {type DeleteApplicationInput, type DeletedResult} from '@sanity/sdk'
2
+ import {expectTypeOf, test} from 'vitest'
3
+
4
+ import {type MutationHookResult} from '../helpers/createMutationHook'
5
+ import {useDeleteApplication} from './useDeleteApplication'
6
+
7
+ test('useDeleteApplication — returns the mutation envelope', () => {
8
+ const result = useDeleteApplication()
9
+ expectTypeOf(result).toEqualTypeOf<MutationHookResult<DeleteApplicationInput, DeletedResult>>()
10
+ expectTypeOf(result.mutate).parameter(0).toEqualTypeOf<DeleteApplicationInput>()
11
+ expectTypeOf(result.data).toEqualTypeOf<DeletedResult | undefined>()
12
+ })
@@ -0,0 +1,11 @@
1
+ import {deleteApplication} from '@sanity/sdk'
2
+
3
+ import {createMutationHook} from '../helpers/createMutationHook'
4
+
5
+ /**
6
+ * Soft-deletes an application.
7
+ *
8
+ * @internal
9
+ * @returns The mutation envelope `{mutate, isPending, error, data, reset}`.
10
+ */
11
+ export const useDeleteApplication = createMutationHook(deleteApplication)
@@ -0,0 +1,14 @@
1
+ import {type DeletedResult, type DeleteUserApplicationInput} from '@sanity/sdk'
2
+ import {expectTypeOf, test} from 'vitest'
3
+
4
+ import {type MutationHookResult} from '../helpers/createMutationHook'
5
+ import {useDeleteUserApplication} from './useDeleteUserApplication'
6
+
7
+ test('useDeleteUserApplication — returns the mutation envelope', () => {
8
+ const result = useDeleteUserApplication()
9
+ expectTypeOf(result).toEqualTypeOf<
10
+ MutationHookResult<DeleteUserApplicationInput, DeletedResult>
11
+ >()
12
+ expectTypeOf(result.mutate).parameter(0).toEqualTypeOf<DeleteUserApplicationInput>()
13
+ expectTypeOf(result.data).toEqualTypeOf<DeletedResult | undefined>()
14
+ })
@@ -0,0 +1,11 @@
1
+ import {deleteUserApplication} from '@sanity/sdk'
2
+
3
+ import {createMutationHook} from '../helpers/createMutationHook'
4
+
5
+ /**
6
+ * Deletes a user application.
7
+ *
8
+ * @internal
9
+ * @returns The mutation envelope `{mutate, isPending, error, data, reset}`.
10
+ */
11
+ export const useDeleteUserApplication = createMutationHook(deleteUserApplication)