@sanity/sdk 0.0.0-alpha.21 → 0.0.0-alpha.23
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 +428 -325
- package/dist/index.js +1618 -1553
- package/dist/index.js.map +1 -1
- package/package.json +6 -7
- package/src/_exports/index.ts +31 -30
- package/src/auth/authStore.test.ts +149 -104
- package/src/auth/authStore.ts +51 -100
- package/src/auth/handleAuthCallback.test.ts +67 -34
- package/src/auth/handleAuthCallback.ts +8 -7
- package/src/auth/logout.test.ts +61 -29
- package/src/auth/logout.ts +26 -28
- package/src/auth/refreshStampedToken.test.ts +9 -9
- package/src/auth/refreshStampedToken.ts +62 -56
- package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +5 -5
- package/src/auth/subscribeToStateAndFetchCurrentUser.ts +45 -47
- package/src/auth/subscribeToStorageEventsAndSetToken.test.ts +4 -5
- package/src/auth/subscribeToStorageEventsAndSetToken.ts +22 -24
- package/src/client/clientStore.test.ts +131 -67
- package/src/client/clientStore.ts +117 -116
- package/src/comlink/controller/actions/destroyController.test.ts +38 -13
- package/src/comlink/controller/actions/destroyController.ts +11 -15
- package/src/comlink/controller/actions/getOrCreateChannel.test.ts +56 -27
- package/src/comlink/controller/actions/getOrCreateChannel.ts +37 -35
- package/src/comlink/controller/actions/getOrCreateController.test.ts +27 -16
- package/src/comlink/controller/actions/getOrCreateController.ts +23 -22
- package/src/comlink/controller/actions/releaseChannel.test.ts +37 -13
- package/src/comlink/controller/actions/releaseChannel.ts +22 -21
- package/src/comlink/controller/comlinkControllerStore.test.ts +65 -36
- package/src/comlink/controller/comlinkControllerStore.ts +44 -5
- package/src/comlink/node/actions/getOrCreateNode.test.ts +31 -15
- package/src/comlink/node/actions/getOrCreateNode.ts +30 -29
- package/src/comlink/node/actions/releaseNode.test.ts +75 -55
- package/src/comlink/node/actions/releaseNode.ts +19 -21
- package/src/comlink/node/comlinkNodeStore.test.ts +6 -11
- package/src/comlink/node/comlinkNodeStore.ts +22 -5
- package/src/config/authConfig.ts +79 -0
- package/src/config/sanityConfig.ts +48 -0
- package/src/datasets/datasets.test.ts +2 -2
- package/src/datasets/datasets.ts +18 -5
- package/src/document/actions.test.ts +22 -10
- package/src/document/actions.ts +44 -56
- package/src/document/applyDocumentActions.test.ts +96 -36
- package/src/document/applyDocumentActions.ts +140 -99
- package/src/document/documentStore.test.ts +103 -155
- package/src/document/documentStore.ts +247 -237
- package/src/document/listen.ts +56 -55
- package/src/document/patchOperations.ts +0 -43
- package/src/document/permissions.test.ts +25 -12
- package/src/document/permissions.ts +11 -4
- package/src/document/processActions.test.ts +41 -8
- package/src/document/reducers.test.ts +87 -16
- package/src/document/reducers.ts +2 -2
- package/src/document/sharedListener.test.ts +34 -16
- package/src/document/sharedListener.ts +33 -11
- package/src/preview/getPreviewState.test.ts +40 -39
- package/src/preview/getPreviewState.ts +68 -56
- package/src/preview/previewConstants.ts +43 -0
- package/src/preview/previewQuery.test.ts +1 -1
- package/src/preview/previewQuery.ts +4 -5
- package/src/preview/previewStore.test.ts +13 -58
- package/src/preview/previewStore.ts +7 -21
- package/src/preview/resolvePreview.test.ts +33 -104
- package/src/preview/resolvePreview.ts +11 -21
- package/src/preview/subscribeToStateAndFetchBatches.test.ts +96 -97
- package/src/preview/subscribeToStateAndFetchBatches.ts +85 -81
- package/src/preview/util.ts +1 -0
- package/src/project/project.test.ts +3 -3
- package/src/project/project.ts +28 -5
- package/src/projection/getProjectionState.test.ts +69 -49
- package/src/projection/getProjectionState.ts +42 -50
- package/src/projection/projectionQuery.ts +1 -1
- package/src/projection/projectionStore.test.ts +13 -51
- package/src/projection/projectionStore.ts +6 -18
- package/src/projection/resolveProjection.test.ts +32 -127
- package/src/projection/resolveProjection.ts +15 -28
- package/src/projection/subscribeToStateAndFetchBatches.test.ts +105 -90
- package/src/projection/subscribeToStateAndFetchBatches.ts +94 -81
- package/src/projection/util.ts +2 -0
- package/src/projects/projects.test.ts +13 -4
- package/src/projects/projects.ts +6 -1
- package/src/query/queryStore.test.ts +10 -47
- package/src/query/queryStore.ts +151 -133
- package/src/query/queryStoreConstants.ts +2 -0
- package/src/store/createActionBinder.test.ts +153 -0
- package/src/store/createActionBinder.ts +176 -0
- package/src/store/createSanityInstance.test.ts +84 -0
- package/src/store/createSanityInstance.ts +124 -0
- package/src/store/createStateSourceAction.test.ts +196 -0
- package/src/store/createStateSourceAction.ts +260 -0
- package/src/store/createStoreInstance.test.ts +81 -0
- package/src/store/createStoreInstance.ts +80 -0
- package/src/store/createStoreState.test.ts +85 -0
- package/src/store/createStoreState.ts +92 -0
- package/src/store/defineStore.test.ts +18 -0
- package/src/store/defineStore.ts +81 -0
- package/src/users/reducers.test.ts +318 -0
- package/src/users/reducers.ts +88 -0
- package/src/users/types.ts +46 -4
- package/src/users/usersConstants.ts +4 -0
- package/src/users/usersStore.test.ts +350 -223
- package/src/users/usersStore.ts +285 -149
- package/src/utils/createFetcherStore.test.ts +6 -7
- package/src/utils/createFetcherStore.ts +150 -153
- package/src/{common/util.test.ts → utils/hashString.test.ts} +1 -1
- package/src/auth/fetchLoginUrls.test.ts +0 -163
- package/src/auth/fetchLoginUrls.ts +0 -74
- package/src/common/createLiveEventSubscriber.test.ts +0 -121
- package/src/common/createLiveEventSubscriber.ts +0 -55
- package/src/common/types.ts +0 -4
- package/src/instance/identity.test.ts +0 -46
- package/src/instance/identity.ts +0 -29
- package/src/instance/sanityInstance.test.ts +0 -77
- package/src/instance/sanityInstance.ts +0 -57
- package/src/instance/types.ts +0 -37
- package/src/preview/getPreviewProjection.ts +0 -45
- package/src/resources/README.md +0 -370
- package/src/resources/createAction.test.ts +0 -101
- package/src/resources/createAction.ts +0 -44
- package/src/resources/createResource.test.ts +0 -112
- package/src/resources/createResource.ts +0 -102
- package/src/resources/createStateSourceAction.test.ts +0 -114
- package/src/resources/createStateSourceAction.ts +0 -83
- package/src/resources/createStore.test.ts +0 -67
- package/src/resources/createStore.ts +0 -46
- package/src/store/createStore.test.ts +0 -108
- package/src/store/createStore.ts +0 -106
- /package/src/{common/util.ts → utils/hashString.ts} +0 -0
|
@@ -1,267 +1,394 @@
|
|
|
1
|
-
import {type
|
|
2
|
-
import {firstValueFrom} from 'rxjs'
|
|
1
|
+
import {type SanityClient} from '@sanity/client'
|
|
2
|
+
import {delay, filter, firstValueFrom, Observable, of} from 'rxjs'
|
|
3
3
|
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import {createSanityInstance} from '../
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
5
|
+
import {getClientState} from '../client/clientStore'
|
|
6
|
+
import {createSanityInstance} from '../store/createSanityInstance'
|
|
7
|
+
import {type StateSource} from '../store/createStateSourceAction'
|
|
8
|
+
import {type GetUsersOptions, type SanityUser, type SanityUserResponse} from './types'
|
|
9
|
+
import {getUsersState, loadMoreUsers, resolveUsers} from './usersStore'
|
|
9
10
|
|
|
10
|
-
vi.mock('
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
data: [],
|
|
14
|
-
totalCount: 0,
|
|
15
|
-
nextCursor: null,
|
|
16
|
-
})),
|
|
17
|
-
}),
|
|
11
|
+
vi.mock('./usersConstants', async (importOriginal) => ({
|
|
12
|
+
...(await importOriginal<typeof import('./usersConstants')>()),
|
|
13
|
+
USERS_STATE_CLEAR_DELAY: 10,
|
|
18
14
|
}))
|
|
19
15
|
|
|
20
|
-
|
|
21
|
-
it('should have correct default initial state', () => {
|
|
22
|
-
const instance = createSanityInstance({projectId: 'test', dataset: 'test'})
|
|
23
|
-
const defaultState = createUsersStore(instance)
|
|
16
|
+
vi.mock('../client/clientStore')
|
|
24
17
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
18
|
+
describe('usersStore', () => {
|
|
19
|
+
let request: SanityClient['observable']['request']
|
|
20
|
+
|
|
21
|
+
const mockUsers: SanityUser[] = [
|
|
22
|
+
{
|
|
23
|
+
sanityUserId: 'user1',
|
|
24
|
+
profile: {
|
|
25
|
+
id: 'profile1',
|
|
26
|
+
displayName: 'User 1',
|
|
27
|
+
email: 'user1@example.com',
|
|
28
|
+
provider: 'google',
|
|
29
|
+
createdAt: '2023-01-01T00:00:00Z',
|
|
35
30
|
},
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
31
|
+
memberships: [
|
|
32
|
+
{
|
|
33
|
+
resourceType: 'project',
|
|
34
|
+
resourceId: 'project1',
|
|
35
|
+
roleNames: ['viewer'],
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
sanityUserId: 'user2',
|
|
41
|
+
profile: {
|
|
42
|
+
id: 'profile2',
|
|
43
|
+
displayName: 'User 2',
|
|
44
|
+
email: 'user2@example.com',
|
|
45
|
+
provider: 'google',
|
|
46
|
+
createdAt: '2023-01-02T00:00:00Z',
|
|
47
|
+
},
|
|
48
|
+
memberships: [
|
|
49
|
+
{
|
|
50
|
+
resourceType: 'project',
|
|
51
|
+
resourceId: 'project1',
|
|
52
|
+
roleNames: ['editor'],
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
]
|
|
39
57
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
58
|
+
const mockResponse: SanityUserResponse = {
|
|
59
|
+
data: mockUsers,
|
|
60
|
+
totalCount: 2,
|
|
61
|
+
nextCursor: null,
|
|
62
|
+
}
|
|
43
63
|
|
|
44
64
|
beforeEach(() => {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
hasMore: false,
|
|
52
|
-
initialFetchCompleted: false,
|
|
53
|
-
options: {
|
|
54
|
-
resourceType: 'organization',
|
|
55
|
-
resourceId: 'org123',
|
|
56
|
-
limit: 100,
|
|
65
|
+
request = vi.fn().mockReturnValue(of(mockResponse).pipe(delay(0)))
|
|
66
|
+
|
|
67
|
+
vi.mocked(getClientState).mockReturnValue({
|
|
68
|
+
observable: of({
|
|
69
|
+
observable: {
|
|
70
|
+
request,
|
|
57
71
|
},
|
|
58
|
-
},
|
|
59
|
-
|
|
60
|
-
)
|
|
72
|
+
} as SanityClient),
|
|
73
|
+
} as StateSource<SanityClient>)
|
|
61
74
|
})
|
|
62
75
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
await expect(firstValueFrom(state$)).resolves.toEqual({
|
|
69
|
-
users: [],
|
|
70
|
-
totalCount: 0,
|
|
71
|
-
nextCursor: null,
|
|
72
|
-
hasMore: false,
|
|
73
|
-
initialFetchCompleted: false,
|
|
74
|
-
options: {
|
|
75
|
-
resourceType: 'organization',
|
|
76
|
-
resourceId: 'org123',
|
|
77
|
-
limit: 100,
|
|
78
|
-
},
|
|
79
|
-
})
|
|
76
|
+
it('initializes users state and cleans up after unsubscribe', async () => {
|
|
77
|
+
const instance = createSanityInstance({projectId: 'test', dataset: 'test'})
|
|
78
|
+
const state = getUsersState(instance, {
|
|
79
|
+
resourceType: 'project',
|
|
80
|
+
projectId: 'project1',
|
|
80
81
|
})
|
|
81
82
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
users: mockUsers,
|
|
97
|
-
totalCount: 1,
|
|
98
|
-
initialFetchCompleted: true,
|
|
99
|
-
})
|
|
83
|
+
// Initially undefined before subscription
|
|
84
|
+
expect(state.getCurrent()).toBeUndefined()
|
|
85
|
+
|
|
86
|
+
// Subscribe to start fetching
|
|
87
|
+
const unsubscribe = state.subscribe()
|
|
88
|
+
|
|
89
|
+
// Wait for data to be fetched
|
|
90
|
+
await firstValueFrom(state.observable.pipe(filter((i) => i !== undefined)))
|
|
91
|
+
|
|
92
|
+
// Verify data is present
|
|
93
|
+
expect(state.getCurrent()).toEqual({
|
|
94
|
+
data: mockUsers,
|
|
95
|
+
totalCount: 2,
|
|
96
|
+
hasMore: false,
|
|
100
97
|
})
|
|
98
|
+
|
|
99
|
+
// Unsubscribe to trigger cleanup
|
|
100
|
+
unsubscribe()
|
|
101
|
+
|
|
102
|
+
// Wait for the cleanup delay
|
|
103
|
+
await new Promise((resolve) => setTimeout(resolve, 20))
|
|
104
|
+
|
|
105
|
+
// Verify state is cleared
|
|
106
|
+
expect(state.getCurrent()).toBeUndefined()
|
|
107
|
+
|
|
108
|
+
instance.dispose()
|
|
101
109
|
})
|
|
102
110
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
nextCursor: 'cursor123',
|
|
109
|
-
}
|
|
110
|
-
vi.mocked(getClient(instance, {apiVersion: 'vX'}).request).mockResolvedValueOnce(mockResponse)
|
|
111
|
-
|
|
112
|
-
const store = createUsersStore({state, instance})
|
|
113
|
-
const result = await store.resolveUsers()
|
|
114
|
-
|
|
115
|
-
expect(getClient(instance, {apiVersion: 'vX'}).request).toHaveBeenCalledWith({
|
|
116
|
-
method: 'GET',
|
|
117
|
-
uri: `access/organization/org123/users`,
|
|
118
|
-
query: {limit: '100'},
|
|
119
|
-
tag: 'users',
|
|
120
|
-
})
|
|
121
|
-
expect(result).toEqual(mockResponse)
|
|
122
|
-
expect(state.get()).toMatchObject({
|
|
123
|
-
users: mockResponse.data,
|
|
124
|
-
totalCount: mockResponse.totalCount,
|
|
125
|
-
nextCursor: mockResponse.nextCursor,
|
|
126
|
-
initialFetchCompleted: true,
|
|
127
|
-
})
|
|
111
|
+
it('maintains state when multiple subscribers exist', async () => {
|
|
112
|
+
const instance = createSanityInstance({projectId: 'test', dataset: 'test'})
|
|
113
|
+
const state = getUsersState(instance, {
|
|
114
|
+
resourceType: 'project',
|
|
115
|
+
projectId: 'project1',
|
|
128
116
|
})
|
|
129
117
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
118
|
+
// Add two subscribers
|
|
119
|
+
const unsubscribe1 = state.subscribe()
|
|
120
|
+
const unsubscribe2 = state.subscribe()
|
|
121
|
+
|
|
122
|
+
// Wait for data to be fetched
|
|
123
|
+
await firstValueFrom(state.observable.pipe(filter((i) => i !== undefined)))
|
|
133
124
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
125
|
+
// Verify data is present
|
|
126
|
+
expect(state.getCurrent()).toEqual({
|
|
127
|
+
data: mockUsers,
|
|
128
|
+
totalCount: 2,
|
|
129
|
+
hasMore: false,
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
// Remove first subscriber
|
|
133
|
+
unsubscribe1()
|
|
134
|
+
|
|
135
|
+
// Data should still be present due to second subscriber
|
|
136
|
+
expect(state.getCurrent()).toEqual({
|
|
137
|
+
data: mockUsers,
|
|
138
|
+
totalCount: 2,
|
|
139
|
+
hasMore: false,
|
|
137
140
|
})
|
|
141
|
+
|
|
142
|
+
// Remove second subscriber
|
|
143
|
+
unsubscribe2()
|
|
144
|
+
|
|
145
|
+
// Wait for cleanup delay
|
|
146
|
+
await new Promise((resolve) => setTimeout(resolve, 20))
|
|
147
|
+
|
|
148
|
+
// Verify state is cleared after all subscribers are gone
|
|
149
|
+
expect(state.getCurrent()).toBeUndefined()
|
|
150
|
+
|
|
151
|
+
instance.dispose()
|
|
138
152
|
})
|
|
139
153
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
totalCount: 15,
|
|
156
|
-
nextCursor: null,
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
const store = createUsersStore({state, instance})
|
|
160
|
-
await store.loadMore()
|
|
161
|
-
|
|
162
|
-
expect(state.get()).toMatchObject({
|
|
163
|
-
users: [...initialUsers, ...newUsers],
|
|
164
|
-
totalCount: 15,
|
|
165
|
-
nextCursor: null,
|
|
166
|
-
hasMore: false,
|
|
167
|
-
})
|
|
154
|
+
it('resolveUsers works without affecting subscriber cleanup', async () => {
|
|
155
|
+
const instance = createSanityInstance({projectId: 'test', dataset: 'test'})
|
|
156
|
+
const options: GetUsersOptions = {resourceType: 'project', projectId: 'project1'}
|
|
157
|
+
|
|
158
|
+
const state = getUsersState(instance, options)
|
|
159
|
+
|
|
160
|
+
// Check that getUsersState starts undefined
|
|
161
|
+
expect(state.getCurrent()).toBeUndefined()
|
|
162
|
+
|
|
163
|
+
// Use resolveUsers which should not add a subscriber
|
|
164
|
+
const result = await resolveUsers(instance, options)
|
|
165
|
+
expect(result).toEqual({
|
|
166
|
+
data: mockUsers,
|
|
167
|
+
totalCount: 2,
|
|
168
|
+
hasMore: false,
|
|
168
169
|
})
|
|
169
170
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
171
|
+
// Check that getUsersState starts resolved now
|
|
172
|
+
expect(state.getCurrent()).toEqual({
|
|
173
|
+
data: mockUsers,
|
|
174
|
+
totalCount: 2,
|
|
175
|
+
hasMore: false,
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
// Subscribing and unsubscribing should clear the state
|
|
179
|
+
const unsubscribe = state.subscribe()
|
|
180
|
+
unsubscribe()
|
|
181
|
+
await new Promise((resolve) => setTimeout(resolve, 20))
|
|
182
|
+
expect(state.getCurrent()).toBeUndefined()
|
|
173
183
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
184
|
+
instance.dispose()
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('handles abort signal in resolveUsers', async () => {
|
|
188
|
+
const instance = createSanityInstance({projectId: 'test', dataset: 'test'})
|
|
189
|
+
const options: GetUsersOptions = {resourceType: 'project', projectId: 'project1'}
|
|
190
|
+
const abortController = new AbortController()
|
|
191
|
+
|
|
192
|
+
// Create a promise that will reject when aborted
|
|
193
|
+
const usersPromise = resolveUsers(instance, {
|
|
194
|
+
...options,
|
|
195
|
+
signal: abortController.signal,
|
|
177
196
|
})
|
|
197
|
+
|
|
198
|
+
// Abort the request
|
|
199
|
+
abortController.abort()
|
|
200
|
+
|
|
201
|
+
// Verify the promise rejects with AbortError
|
|
202
|
+
await expect(usersPromise).rejects.toThrow('The operation was aborted.')
|
|
203
|
+
|
|
204
|
+
// Verify state is cleared after abort
|
|
205
|
+
expect(getUsersState(instance, options).getCurrent()).toBeUndefined()
|
|
206
|
+
|
|
207
|
+
instance.dispose()
|
|
178
208
|
})
|
|
179
209
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
210
|
+
it('loads more users when loadMoreUsers is called', async () => {
|
|
211
|
+
const instance = createSanityInstance({projectId: 'test', dataset: 'test'})
|
|
212
|
+
const options: GetUsersOptions = {resourceType: 'project', projectId: 'project1'}
|
|
213
|
+
|
|
214
|
+
// First response has nextCursor
|
|
215
|
+
const firstResponse = {
|
|
216
|
+
...mockResponse,
|
|
217
|
+
nextCursor: 'next-page',
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Additional users for the second page
|
|
221
|
+
const additionalUsers: SanityUser[] = [
|
|
222
|
+
{
|
|
223
|
+
sanityUserId: 'user3',
|
|
224
|
+
profile: {
|
|
225
|
+
id: 'profile3',
|
|
226
|
+
displayName: 'User 3',
|
|
227
|
+
email: 'user3@example.com',
|
|
228
|
+
provider: 'google',
|
|
229
|
+
createdAt: '2023-01-03T00:00:00Z',
|
|
230
|
+
},
|
|
231
|
+
memberships: [
|
|
232
|
+
{
|
|
233
|
+
resourceType: 'project',
|
|
234
|
+
resourceId: 'project1',
|
|
235
|
+
roleNames: ['admin'],
|
|
236
|
+
},
|
|
237
|
+
],
|
|
238
|
+
},
|
|
239
|
+
]
|
|
240
|
+
|
|
241
|
+
// Second response has no nextCursor
|
|
242
|
+
const secondResponse = {
|
|
243
|
+
data: additionalUsers,
|
|
244
|
+
totalCount: 3,
|
|
245
|
+
nextCursor: null,
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Setup request mock to return different responses
|
|
249
|
+
vi.mocked(request).mockReset()
|
|
250
|
+
vi.mocked(request).mockImplementationOnce(() => of(firstResponse).pipe(delay(0)))
|
|
251
|
+
vi.mocked(request).mockImplementationOnce(() => of(secondResponse).pipe(delay(0)))
|
|
252
|
+
|
|
253
|
+
const state = getUsersState(instance, options)
|
|
254
|
+
const unsubscribe = state.subscribe()
|
|
255
|
+
|
|
256
|
+
// Wait for initial data
|
|
257
|
+
await firstValueFrom(state.observable.pipe(filter((i) => i !== undefined)))
|
|
258
|
+
|
|
259
|
+
// Verify initial data
|
|
260
|
+
expect(state.getCurrent()).toEqual({
|
|
261
|
+
data: mockUsers,
|
|
262
|
+
totalCount: 2,
|
|
263
|
+
hasMore: true,
|
|
192
264
|
})
|
|
193
265
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
users: [],
|
|
203
|
-
totalCount: 0,
|
|
204
|
-
nextCursor: null,
|
|
205
|
-
hasMore: false,
|
|
206
|
-
initialFetchCompleted: false,
|
|
207
|
-
})
|
|
266
|
+
// Load more users
|
|
267
|
+
await loadMoreUsers(instance, options)
|
|
268
|
+
|
|
269
|
+
// Verify updated data includes both pages
|
|
270
|
+
expect(state.getCurrent()).toEqual({
|
|
271
|
+
data: [...mockUsers, ...additionalUsers],
|
|
272
|
+
totalCount: 3,
|
|
273
|
+
hasMore: false,
|
|
208
274
|
})
|
|
209
275
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
276
|
+
unsubscribe()
|
|
277
|
+
instance.dispose()
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
it('throws error when loadMoreUsers is called without initial data', async () => {
|
|
281
|
+
const instance = createSanityInstance({projectId: 'test', dataset: 'test'})
|
|
282
|
+
|
|
283
|
+
// Expect loadMoreUsers to throw when no data is loaded
|
|
284
|
+
await expect(
|
|
285
|
+
loadMoreUsers(instance, {resourceType: 'project', projectId: 'project1'}),
|
|
286
|
+
).rejects.toThrow('Users not loaded for specified resource')
|
|
287
|
+
|
|
288
|
+
instance.dispose()
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
it('throws error when loadMoreUsers is called with no more data available', async () => {
|
|
292
|
+
const instance = createSanityInstance({projectId: 'test', dataset: 'test'})
|
|
293
|
+
const options: GetUsersOptions = {resourceType: 'project', projectId: 'project1'}
|
|
294
|
+
|
|
295
|
+
// Response with no nextCursor
|
|
296
|
+
vi.mocked(request).mockReset()
|
|
297
|
+
vi.mocked(request).mockImplementationOnce(() => of(mockResponse).pipe(delay(0)))
|
|
298
|
+
|
|
299
|
+
const state = getUsersState(instance, options)
|
|
300
|
+
const unsubscribe = state.subscribe()
|
|
301
|
+
|
|
302
|
+
// Wait for data to be fetched
|
|
303
|
+
await firstValueFrom(state.observable.pipe(filter((i) => i !== undefined)))
|
|
304
|
+
|
|
305
|
+
// Expect loadMoreUsers to throw when hasMore is false
|
|
306
|
+
await expect(loadMoreUsers(instance, options)).rejects.toThrow(
|
|
307
|
+
'No more users available to load for this resource',
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
unsubscribe()
|
|
311
|
+
instance.dispose()
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
it('handles errors in users fetching', async () => {
|
|
315
|
+
const instance = createSanityInstance({projectId: 'test', dataset: 'test'})
|
|
316
|
+
const errorMessage = 'Failed to fetch users'
|
|
317
|
+
|
|
318
|
+
// Override request to simulate error
|
|
319
|
+
vi.mocked(request).mockReset()
|
|
320
|
+
vi.mocked(request).mockImplementationOnce(
|
|
321
|
+
() =>
|
|
322
|
+
new Observable((observer) => {
|
|
323
|
+
observer.error(new Error(errorMessage))
|
|
324
|
+
}),
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
const state = getUsersState(instance, {
|
|
328
|
+
resourceType: 'project',
|
|
329
|
+
projectId: 'project1',
|
|
222
330
|
})
|
|
331
|
+
const unsubscribe = state.subscribe()
|
|
332
|
+
|
|
333
|
+
// Verify error is thrown when accessing state
|
|
334
|
+
expect(() => state.getCurrent()).toThrow(errorMessage)
|
|
335
|
+
|
|
336
|
+
unsubscribe()
|
|
337
|
+
instance.dispose()
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
it('delays users state removal after unsubscribe', async () => {
|
|
341
|
+
const instance = createSanityInstance({projectId: 'test', dataset: 'test'})
|
|
342
|
+
const options: GetUsersOptions = {resourceType: 'project', projectId: 'project1'}
|
|
343
|
+
const state = getUsersState(instance, options)
|
|
344
|
+
const unsubscribe = state.subscribe()
|
|
345
|
+
|
|
346
|
+
await firstValueFrom(state.observable.pipe(filter((i) => i !== undefined)))
|
|
347
|
+
|
|
348
|
+
unsubscribe()
|
|
349
|
+
// Immediately after unsubscription, state should still be present due to delay
|
|
350
|
+
expect(state.getCurrent()).not.toBeUndefined()
|
|
351
|
+
|
|
352
|
+
// Wait for the cleanup delay and then state should be removed
|
|
353
|
+
await new Promise((resolve) => setTimeout(resolve, 20))
|
|
354
|
+
expect(state.getCurrent()).toBeUndefined()
|
|
355
|
+
|
|
356
|
+
instance.dispose()
|
|
223
357
|
})
|
|
224
358
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
nextCursor: null,
|
|
231
|
-
})
|
|
232
|
-
|
|
233
|
-
const store = createUsersStore({state, instance})
|
|
234
|
-
await store.resolveUsers()
|
|
235
|
-
|
|
236
|
-
expect(state.get()).toMatchObject({
|
|
237
|
-
users: [],
|
|
238
|
-
totalCount: 0,
|
|
239
|
-
initialFetchCompleted: true,
|
|
240
|
-
})
|
|
359
|
+
it('preserves users state if a new subscriber subscribes before cleanup delay', async () => {
|
|
360
|
+
const instance = createSanityInstance({projectId: 'test', dataset: 'test'})
|
|
361
|
+
const state = getUsersState(instance, {
|
|
362
|
+
resourceType: 'project',
|
|
363
|
+
projectId: 'project1',
|
|
241
364
|
})
|
|
365
|
+
const unsubscribe1 = state.subscribe()
|
|
242
366
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
initialFetchCompleted: true,
|
|
250
|
-
})
|
|
367
|
+
await firstValueFrom(state.observable.pipe(filter((i) => i !== undefined)))
|
|
368
|
+
expect(state.getCurrent()).toEqual({
|
|
369
|
+
data: mockUsers,
|
|
370
|
+
totalCount: 2,
|
|
371
|
+
hasMore: false,
|
|
372
|
+
})
|
|
251
373
|
|
|
252
|
-
|
|
253
|
-
|
|
374
|
+
unsubscribe1()
|
|
375
|
+
// Wait less than the cleanup delay
|
|
376
|
+
await new Promise((resolve) => setTimeout(resolve, 5))
|
|
254
377
|
|
|
255
|
-
|
|
256
|
-
|
|
378
|
+
// Subscribe again before cleanup occurs
|
|
379
|
+
const unsubscribe2 = state.subscribe()
|
|
380
|
+
|
|
381
|
+
// Wait for cleanup delay to pass
|
|
382
|
+
await new Promise((resolve) => setTimeout(resolve, 20))
|
|
383
|
+
|
|
384
|
+
// Since a subscriber now exists, state should still be present
|
|
385
|
+
expect(state.getCurrent()).toEqual({
|
|
386
|
+
data: mockUsers,
|
|
387
|
+
totalCount: 2,
|
|
388
|
+
hasMore: false,
|
|
257
389
|
})
|
|
258
|
-
})
|
|
259
|
-
})
|
|
260
390
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
store.setOptions({resourceType: 'organization', resourceId: 'org123'})
|
|
265
|
-
store.resolveUsers()
|
|
266
|
-
expect(getClient).toHaveBeenCalledWith(instance, {apiVersion: 'vX', scope: 'global'})
|
|
391
|
+
unsubscribe2()
|
|
392
|
+
instance.dispose()
|
|
393
|
+
})
|
|
267
394
|
})
|