@sanity/sdk-react 2.14.1 → 2.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/README.md +40 -30
  2. package/dist/index.d.ts +512 -335
  3. package/dist/index.js +540 -332
  4. package/dist/index.js.map +1 -1
  5. package/package.json +37 -39
  6. package/src/_exports/sdk-react.ts +1 -0
  7. package/src/components/SDKProvider.test.tsx +135 -22
  8. package/src/components/SDKProvider.tsx +54 -17
  9. package/src/components/SanityApp.tsx +29 -0
  10. package/src/components/auth/AuthBoundary.recovery.test.tsx +86 -0
  11. package/src/components/auth/AuthBoundary.tsx +11 -1
  12. package/src/context/OrganizationResourcesProvider.test.tsx +189 -0
  13. package/src/context/OrganizationResourcesProvider.tsx +111 -0
  14. package/src/context/ProjectContext.ts +15 -0
  15. package/src/context/ResourceProvider.test.tsx +57 -1
  16. package/src/context/ResourceProvider.tsx +30 -9
  17. package/src/hooks/datasets/useDatasets.test.tsx +116 -0
  18. package/src/hooks/datasets/useDatasets.ts +20 -8
  19. package/src/hooks/document/useApplyDocumentActions.ts +1 -1
  20. package/src/hooks/document/useCreateDocument.test.tsx +83 -0
  21. package/src/hooks/document/useCreateDocument.ts +117 -0
  22. package/src/hooks/document/useDocument.ts +2 -2
  23. package/src/hooks/helpers/useResolvedProjectId.test.tsx +59 -0
  24. package/src/hooks/helpers/useResolvedProjectId.ts +35 -0
  25. package/src/hooks/projects/useProject.test.tsx +114 -0
  26. package/src/hooks/projects/useProject.ts +17 -7
  27. package/src/hooks/users/useUsers.test.tsx +101 -2
  28. package/src/hooks/users/useUsers.ts +35 -3
  29. package/src/utils/resolveOrgResources.test.ts +111 -0
  30. package/src/utils/resolveOrgResources.ts +69 -0
  31. package/src/hooks/datasets/useDatasets.test.ts +0 -80
  32. package/src/hooks/projects/useProject.test.ts +0 -80
@@ -0,0 +1,111 @@
1
+ import {type SanityClient} from '@sanity/client'
2
+ import {getClientState, type SanityInstance, type StateSource} from '@sanity/sdk'
3
+ import {firstValueFrom} from 'rxjs'
4
+ import {beforeEach, describe, expect, it, vi} from 'vitest'
5
+
6
+ import {resolveOrgResources} from './resolveOrgResources'
7
+
8
+ vi.mock('@sanity/sdk', () => ({
9
+ getClientState: vi.fn(),
10
+ }))
11
+
12
+ vi.mock('rxjs', () => ({
13
+ firstValueFrom: vi.fn(),
14
+ }))
15
+
16
+ const mockGetClientState = vi.mocked(getClientState)
17
+ const mockFirstValueFrom = vi.mocked(firstValueFrom)
18
+
19
+ const mockRequest = vi.fn()
20
+ const mockClient = {request: mockRequest}
21
+ const mockInstance = {} as SanityInstance
22
+
23
+ beforeEach(() => {
24
+ vi.clearAllMocks()
25
+ mockGetClientState.mockReturnValue({
26
+ observable: 'mock-observable',
27
+ } as unknown as StateSource<SanityClient>)
28
+ mockFirstValueFrom.mockResolvedValue(mockClient as unknown as SanityClient)
29
+ })
30
+
31
+ const ORG_ID = 'org-1'
32
+
33
+ describe('resolveOrgResources', () => {
34
+ it('returns both mediaLibrary and canvas when both requests succeed', async () => {
35
+ mockRequest
36
+ .mockResolvedValueOnce({data: [{id: 'ml-123'}]})
37
+ .mockResolvedValueOnce({data: [{id: 'canvas-456'}]})
38
+
39
+ const result = await resolveOrgResources(mockInstance, ORG_ID)
40
+
41
+ expect(result).toEqual({
42
+ mediaLibrary: {mediaLibraryId: 'ml-123'},
43
+ canvas: {canvasId: 'canvas-456'},
44
+ })
45
+ })
46
+
47
+ it('scopes both requests to the organization', async () => {
48
+ mockRequest
49
+ .mockResolvedValueOnce({data: [{id: 'ml-123'}]})
50
+ .mockResolvedValueOnce({data: [{id: 'canvas-456'}]})
51
+
52
+ await resolveOrgResources(mockInstance, ORG_ID)
53
+
54
+ expect(mockRequest).toHaveBeenCalledWith(
55
+ expect.objectContaining({uri: '/media-libraries', query: {organizationId: ORG_ID}}),
56
+ )
57
+ expect(mockRequest).toHaveBeenCalledWith(
58
+ expect.objectContaining({uri: '/canvases', query: {organizationId: ORG_ID}}),
59
+ )
60
+ })
61
+
62
+ it('returns only mediaLibrary when canvas request fails', async () => {
63
+ mockRequest
64
+ .mockResolvedValueOnce({data: [{id: 'ml-123'}]})
65
+ .mockRejectedValueOnce(new Error('canvas not found'))
66
+
67
+ const result = await resolveOrgResources(mockInstance, ORG_ID)
68
+
69
+ expect(result).toEqual({
70
+ mediaLibrary: {mediaLibraryId: 'ml-123'},
71
+ canvas: undefined,
72
+ })
73
+ })
74
+
75
+ it('returns only canvas when mediaLibrary request fails', async () => {
76
+ mockRequest
77
+ .mockRejectedValueOnce(new Error('media library not found'))
78
+ .mockResolvedValueOnce({data: [{id: 'canvas-456'}]})
79
+
80
+ const result = await resolveOrgResources(mockInstance, ORG_ID)
81
+
82
+ expect(result).toEqual({
83
+ mediaLibrary: undefined,
84
+ canvas: {canvasId: 'canvas-456'},
85
+ })
86
+ })
87
+
88
+ it('returns undefined for both when both requests fail', async () => {
89
+ mockRequest
90
+ .mockRejectedValueOnce(new Error('error 1'))
91
+ .mockRejectedValueOnce(new Error('error 2'))
92
+
93
+ const result = await resolveOrgResources(mockInstance, ORG_ID)
94
+
95
+ expect(result).toEqual({
96
+ mediaLibrary: undefined,
97
+ canvas: undefined,
98
+ })
99
+ })
100
+
101
+ it('returns undefined for both when data arrays are empty', async () => {
102
+ mockRequest.mockResolvedValueOnce({data: []}).mockResolvedValueOnce({data: []})
103
+
104
+ const result = await resolveOrgResources(mockInstance, ORG_ID)
105
+
106
+ expect(result).toEqual({
107
+ mediaLibrary: undefined,
108
+ canvas: undefined,
109
+ })
110
+ })
111
+ })
@@ -0,0 +1,69 @@
1
+ import {
2
+ type CanvasResource,
3
+ getClientState,
4
+ type MediaLibraryResource,
5
+ type SanityInstance,
6
+ } from '@sanity/sdk'
7
+ import {firstValueFrom} from 'rxjs'
8
+
9
+ const API_VERSION = 'v2026-07-09'
10
+
11
+ interface OrgResourcesApiItem {
12
+ id: string
13
+ }
14
+
15
+ interface OrgResourcesApiResponse {
16
+ data: OrgResourcesApiItem[]
17
+ }
18
+
19
+ interface OrgResources {
20
+ mediaLibrary?: MediaLibraryResource
21
+ canvas?: CanvasResource
22
+ }
23
+
24
+ /**
25
+ * Gets the id of the first resource from a settled request result. The request
26
+ * is already scoped to a single organization, so the first item is the one we want.
27
+ */
28
+ function getFirstResourceId(
29
+ result: PromiseSettledResult<OrgResourcesApiResponse>,
30
+ ): string | undefined {
31
+ if (result.status !== 'fulfilled') return undefined
32
+ return result.value.data?.[0]?.id
33
+ }
34
+
35
+ /**
36
+ * Fetches the media library and canvas resources for the given organization.
37
+ * Both requests are scoped to `organizationId` so each response contains only
38
+ * that org's resources. Each resource is fetched independently — a failure for
39
+ * one does not prevent the other from resolving.
40
+ */
41
+ export async function resolveOrgResources(
42
+ instance: SanityInstance,
43
+ organizationId: string,
44
+ ): Promise<OrgResources> {
45
+ const client = await firstValueFrom(
46
+ getClientState(instance, {apiVersion: API_VERSION, scope: 'global'}).observable,
47
+ )
48
+
49
+ const [mediaLibrariesResult, canvasesResult] = await Promise.allSettled([
50
+ client.request<OrgResourcesApiResponse>({
51
+ uri: `/media-libraries`,
52
+ query: {organizationId},
53
+ tag: 'org-resources.media-libraries',
54
+ }),
55
+ client.request<OrgResourcesApiResponse>({
56
+ uri: `/canvases`,
57
+ query: {organizationId},
58
+ tag: 'org-resources.canvases',
59
+ }),
60
+ ])
61
+
62
+ const mediaLibraryId = getFirstResourceId(mediaLibrariesResult)
63
+ const canvasId = getFirstResourceId(canvasesResult)
64
+
65
+ return {
66
+ mediaLibrary: mediaLibraryId ? {mediaLibraryId} : undefined,
67
+ canvas: canvasId ? {canvasId} : undefined,
68
+ }
69
+ }
@@ -1,80 +0,0 @@
1
- import {getDatasetsState, type ProjectHandle, type SanityInstance} from '@sanity/sdk'
2
- import {beforeEach, describe, expect, it, vi} from 'vitest'
3
-
4
- import {createStateSourceHook} from '../helpers/createStateSourceHook'
5
-
6
- // Mock dependencies
7
- vi.mock('@sanity/sdk', () => ({
8
- getDatasetsState: vi.fn(() => ({
9
- getCurrent: vi.fn(() => undefined), // Mocking getCurrent to satisfy the call within shouldSuspend
10
- })),
11
- resolveDatasets: vi.fn(),
12
- }))
13
- vi.mock('../helpers/createStateSourceHook', () => ({
14
- createStateSourceHook: vi.fn(),
15
- }))
16
-
17
- describe('useDatasets', () => {
18
- // Use beforeEach to reset modules and ensure mocks are fresh for each test
19
- beforeEach(() => {
20
- vi.resetModules()
21
- // Re-mock dependencies for each test after resetModules
22
- vi.mock('@sanity/sdk', () => ({
23
- getDatasetsState: vi.fn(() => ({
24
- getCurrent: vi.fn(() => undefined),
25
- })),
26
- resolveDatasets: vi.fn(),
27
- }))
28
- vi.mock('../helpers/createStateSourceHook', () => ({
29
- createStateSourceHook: vi.fn(),
30
- }))
31
- })
32
-
33
- it('should call createStateSourceHook with correct arguments on import', async () => {
34
- // Dynamically import the hook *after* mocks are set up and modules reset
35
- await import('./useDatasets')
36
-
37
- // Check if createStateSourceHook was called during the module evaluation (import)
38
- expect(createStateSourceHook).toHaveBeenCalled()
39
- expect(createStateSourceHook).toHaveBeenCalledWith(
40
- expect.objectContaining({
41
- getState: expect.any(Function),
42
- shouldSuspend: expect.any(Function),
43
- suspender: expect.any(Function), // Actual function reference doesn't matter here as it's mocked
44
- getConfig: expect.any(Function), // Actual function reference doesn't matter here
45
- }),
46
- )
47
- })
48
-
49
- it('shouldSuspend should call getDatasetsState and getCurrent', async () => {
50
- // Dynamically import the hook *after* mocks are set up and modules reset
51
- await import('./useDatasets')
52
-
53
- // Get the arguments passed to createStateSourceHook
54
- // Need to ensure createStateSourceHook mock is correctly typed for access
55
- const mockCreateStateSourceHook = createStateSourceHook as ReturnType<typeof vi.fn>
56
- expect(mockCreateStateSourceHook.mock.calls.length).toBeGreaterThan(0)
57
- const createStateSourceHookArgs = mockCreateStateSourceHook.mock.calls[0][0]
58
- const shouldSuspend = createStateSourceHookArgs.shouldSuspend
59
-
60
- // Mock instance and projectHandle for the test call
61
- const mockInstance = {} as SanityInstance // Use specific type
62
- const mockProjectHandle = {} as ProjectHandle // Use specific type
63
-
64
- // Call the shouldSuspend function
65
- const result = shouldSuspend(mockInstance, mockProjectHandle)
66
-
67
- // Assert that getDatasetsState was called with the correct arguments
68
- // Need to ensure getDatasetsState mock is correctly typed for access
69
- const mockGetDatasetsState = getDatasetsState as ReturnType<typeof vi.fn>
70
- expect(mockGetDatasetsState).toHaveBeenCalledWith(mockInstance, mockProjectHandle)
71
-
72
- // Assert that getCurrent was called on the result of getDatasetsState
73
- expect(mockGetDatasetsState.mock.results.length).toBeGreaterThan(0)
74
- const getDatasetsStateMockResult = mockGetDatasetsState.mock.results[0].value
75
- expect(getDatasetsStateMockResult.getCurrent).toHaveBeenCalled()
76
-
77
- // Assert the result of shouldSuspend based on the mocked getCurrent value
78
- expect(result).toBe(true) // Since getCurrent is mocked to return undefined
79
- })
80
- })
@@ -1,80 +0,0 @@
1
- import {getProjectState, type ProjectHandle, type SanityInstance} from '@sanity/sdk'
2
- import {beforeEach, describe, expect, it, vi} from 'vitest'
3
-
4
- import {createStateSourceHook} from '../helpers/createStateSourceHook'
5
-
6
- // Mock dependencies
7
- vi.mock('@sanity/sdk', () => ({
8
- getProjectState: vi.fn(() => ({
9
- getCurrent: vi.fn(() => undefined), // Mocking getCurrent to satisfy the call within shouldSuspend
10
- })),
11
- resolveProject: vi.fn(),
12
- }))
13
- vi.mock('../helpers/createStateSourceHook', () => ({
14
- createStateSourceHook: vi.fn(),
15
- }))
16
-
17
- describe('useProject', () => {
18
- // Use beforeEach to reset modules and ensure mocks are fresh for each test
19
- beforeEach(() => {
20
- vi.resetModules()
21
- // Re-mock dependencies for each test after resetModules
22
- vi.mock('@sanity/sdk', () => ({
23
- getProjectState: vi.fn(() => ({
24
- getCurrent: vi.fn(() => undefined),
25
- })),
26
- resolveProject: vi.fn(),
27
- }))
28
- vi.mock('../helpers/createStateSourceHook', () => ({
29
- createStateSourceHook: vi.fn(),
30
- }))
31
- })
32
-
33
- it('should call createStateSourceHook with correct arguments on import', async () => {
34
- // Dynamically import the hook *after* mocks are set up and modules reset
35
- await import('./useProject')
36
-
37
- // Check if createStateSourceHook was called during the module evaluation (import)
38
- expect(createStateSourceHook).toHaveBeenCalled()
39
- expect(createStateSourceHook).toHaveBeenCalledWith(
40
- expect.objectContaining({
41
- getState: expect.any(Function),
42
- shouldSuspend: expect.any(Function),
43
- suspender: expect.any(Function), // Actual function reference doesn't matter here as it's mocked
44
- getConfig: expect.any(Function), // Actual function reference doesn't matter here
45
- }),
46
- )
47
- })
48
-
49
- it('shouldSuspend should call getProjectState and getCurrent', async () => {
50
- // Dynamically import the hook *after* mocks are set up and modules reset
51
- await import('./useProject')
52
-
53
- // Get the arguments passed to createStateSourceHook
54
- // Need to ensure createStateSourceHook mock is correctly typed for access
55
- const mockCreateStateSourceHook = createStateSourceHook as ReturnType<typeof vi.fn>
56
- expect(mockCreateStateSourceHook.mock.calls.length).toBeGreaterThan(0)
57
- const createStateSourceHookArgs = mockCreateStateSourceHook.mock.calls[0][0]
58
- const shouldSuspend = createStateSourceHookArgs.shouldSuspend
59
-
60
- // Mock instance and projectHandle for the test call
61
- const mockInstance = {} as SanityInstance // Use specific type
62
- const mockProjectHandle = {} as ProjectHandle // Use specific type
63
-
64
- // Call the shouldSuspend function
65
- const result = shouldSuspend(mockInstance, mockProjectHandle)
66
-
67
- // Assert that getProjectState was called with the correct arguments
68
- // Need to ensure getProjectState mock is correctly typed for access
69
- const mockGetProjectState = getProjectState as ReturnType<typeof vi.fn>
70
- expect(mockGetProjectState).toHaveBeenCalledWith(mockInstance, mockProjectHandle)
71
-
72
- // Assert that getCurrent was called on the result of getProjectState
73
- expect(mockGetProjectState.mock.results.length).toBeGreaterThan(0)
74
- const getProjectStateMockResult = mockGetProjectState.mock.results[0].value
75
- expect(getProjectStateMockResult.getCurrent).toHaveBeenCalled()
76
-
77
- // Assert the result of shouldSuspend based on the mocked getCurrent value
78
- expect(result).toBe(true) // Since getCurrent is mocked to return undefined
79
- })
80
- })