@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.
- package/dist/index.d.ts +267 -112
- package/dist/index.js +131 -89
- package/dist/index.js.map +1 -1
- package/package.json +11 -9
- package/src/_exports/sdk-react.ts +14 -1
- package/src/components/auth/AuthBoundary.tsx +10 -5
- package/src/context/WorkbenchTokenRefresh.test.tsx +106 -0
- package/src/context/WorkbenchTokenRefresh.tsx +61 -0
- package/src/context/workbenchToken.ts +63 -0
- package/src/hooks/access/useCheckPermissions.test-d.ts +15 -0
- package/src/hooks/access/useCheckPermissions.test.tsx +53 -0
- package/src/hooks/access/useCheckPermissions.ts +24 -0
- package/src/hooks/applications/useApplication.test-d.ts +30 -0
- package/src/hooks/applications/useApplication.ts +22 -0
- package/src/hooks/applications/useApplications.test-d.ts +31 -0
- package/src/hooks/applications/useApplications.ts +25 -0
- package/src/hooks/applications/useCreateUserApplication.test-d.ts +14 -0
- package/src/hooks/applications/useCreateUserApplication.ts +11 -0
- package/src/hooks/applications/useDeleteApplication.test-d.ts +12 -0
- package/src/hooks/applications/useDeleteApplication.ts +11 -0
- package/src/hooks/applications/useDeleteUserApplication.test-d.ts +14 -0
- package/src/hooks/applications/useDeleteUserApplication.ts +11 -0
- package/src/hooks/applications/useUpdateApplication.test-d.ts +12 -0
- package/src/hooks/applications/useUpdateApplication.ts +11 -0
- package/src/hooks/applications/useUpdateUserApplication.test-d.ts +14 -0
- package/src/hooks/applications/useUpdateUserApplication.ts +11 -0
- package/src/hooks/applications/useUserApplication.test-d.ts +11 -0
- package/src/hooks/applications/useUserApplication.ts +14 -0
- package/src/hooks/applications/useUserApplications.test-d.ts +11 -0
- package/src/hooks/applications/useUserApplications.ts +14 -0
- package/src/hooks/helpers/createFetcherHook.test.tsx +180 -0
- package/src/hooks/helpers/createFetcherHook.ts +69 -0
- package/src/hooks/helpers/createMutationHook.test.tsx +125 -0
- package/src/hooks/helpers/createMutationHook.tsx +93 -0
- package/src/hooks/installations/useInstallation.test-d.ts +19 -0
- package/src/hooks/installations/useInstallation.ts +22 -0
- package/src/hooks/installations/useInstallations.test-d.ts +26 -0
- package/src/hooks/installations/useInstallations.ts +25 -0
- package/src/hooks/organizations/useOrganization.test-d.ts +19 -12
- package/src/hooks/organizations/useOrganization.test.ts +50 -52
- package/src/hooks/organizations/useOrganization.ts +13 -19
- package/src/hooks/organizations/useOrganizations.test-d.ts +26 -13
- package/src/hooks/organizations/useOrganizations.test.ts +49 -71
- package/src/hooks/organizations/useOrganizations.ts +14 -20
- package/src/hooks/projects/useProject.test-d.ts +18 -11
- package/src/hooks/projects/useProject.test.tsx +29 -23
- package/src/hooks/projects/useProject.ts +12 -16
- package/src/hooks/projects/useProjects.test-d.ts +22 -11
- package/src/hooks/projects/useProjects.test.ts +45 -98
- package/src/hooks/projects/useProjects.ts +16 -17
- package/src/hooks/dashboard/useDispatchIntent.test.ts +0 -251
- package/src/hooks/dashboard/useDispatchIntent.ts +0 -157
- package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.test.ts +0 -128
- 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": "
|
|
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.
|
|
47
|
+
"@sanity/client": "^7.23.2",
|
|
48
48
|
"@sanity/message-protocol": "^0.23.0",
|
|
49
|
-
"@sanity/types": "^6.
|
|
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
|
-
"
|
|
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.
|
|
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": "^
|
|
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.
|
|
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/
|
|
82
|
-
"@repo/
|
|
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
|
-
<
|
|
145
|
-
<
|
|
146
|
-
|
|
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
|
|
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)
|