@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.
Files changed (127) hide show
  1. package/dist/index.d.ts +428 -325
  2. package/dist/index.js +1618 -1553
  3. package/dist/index.js.map +1 -1
  4. package/package.json +6 -7
  5. package/src/_exports/index.ts +31 -30
  6. package/src/auth/authStore.test.ts +149 -104
  7. package/src/auth/authStore.ts +51 -100
  8. package/src/auth/handleAuthCallback.test.ts +67 -34
  9. package/src/auth/handleAuthCallback.ts +8 -7
  10. package/src/auth/logout.test.ts +61 -29
  11. package/src/auth/logout.ts +26 -28
  12. package/src/auth/refreshStampedToken.test.ts +9 -9
  13. package/src/auth/refreshStampedToken.ts +62 -56
  14. package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +5 -5
  15. package/src/auth/subscribeToStateAndFetchCurrentUser.ts +45 -47
  16. package/src/auth/subscribeToStorageEventsAndSetToken.test.ts +4 -5
  17. package/src/auth/subscribeToStorageEventsAndSetToken.ts +22 -24
  18. package/src/client/clientStore.test.ts +131 -67
  19. package/src/client/clientStore.ts +117 -116
  20. package/src/comlink/controller/actions/destroyController.test.ts +38 -13
  21. package/src/comlink/controller/actions/destroyController.ts +11 -15
  22. package/src/comlink/controller/actions/getOrCreateChannel.test.ts +56 -27
  23. package/src/comlink/controller/actions/getOrCreateChannel.ts +37 -35
  24. package/src/comlink/controller/actions/getOrCreateController.test.ts +27 -16
  25. package/src/comlink/controller/actions/getOrCreateController.ts +23 -22
  26. package/src/comlink/controller/actions/releaseChannel.test.ts +37 -13
  27. package/src/comlink/controller/actions/releaseChannel.ts +22 -21
  28. package/src/comlink/controller/comlinkControllerStore.test.ts +65 -36
  29. package/src/comlink/controller/comlinkControllerStore.ts +44 -5
  30. package/src/comlink/node/actions/getOrCreateNode.test.ts +31 -15
  31. package/src/comlink/node/actions/getOrCreateNode.ts +30 -29
  32. package/src/comlink/node/actions/releaseNode.test.ts +75 -55
  33. package/src/comlink/node/actions/releaseNode.ts +19 -21
  34. package/src/comlink/node/comlinkNodeStore.test.ts +6 -11
  35. package/src/comlink/node/comlinkNodeStore.ts +22 -5
  36. package/src/config/authConfig.ts +79 -0
  37. package/src/config/sanityConfig.ts +48 -0
  38. package/src/datasets/datasets.test.ts +2 -2
  39. package/src/datasets/datasets.ts +18 -5
  40. package/src/document/actions.test.ts +22 -10
  41. package/src/document/actions.ts +44 -56
  42. package/src/document/applyDocumentActions.test.ts +96 -36
  43. package/src/document/applyDocumentActions.ts +140 -99
  44. package/src/document/documentStore.test.ts +103 -155
  45. package/src/document/documentStore.ts +247 -237
  46. package/src/document/listen.ts +56 -55
  47. package/src/document/patchOperations.ts +0 -43
  48. package/src/document/permissions.test.ts +25 -12
  49. package/src/document/permissions.ts +11 -4
  50. package/src/document/processActions.test.ts +41 -8
  51. package/src/document/reducers.test.ts +87 -16
  52. package/src/document/reducers.ts +2 -2
  53. package/src/document/sharedListener.test.ts +34 -16
  54. package/src/document/sharedListener.ts +33 -11
  55. package/src/preview/getPreviewState.test.ts +40 -39
  56. package/src/preview/getPreviewState.ts +68 -56
  57. package/src/preview/previewConstants.ts +43 -0
  58. package/src/preview/previewQuery.test.ts +1 -1
  59. package/src/preview/previewQuery.ts +4 -5
  60. package/src/preview/previewStore.test.ts +13 -58
  61. package/src/preview/previewStore.ts +7 -21
  62. package/src/preview/resolvePreview.test.ts +33 -104
  63. package/src/preview/resolvePreview.ts +11 -21
  64. package/src/preview/subscribeToStateAndFetchBatches.test.ts +96 -97
  65. package/src/preview/subscribeToStateAndFetchBatches.ts +85 -81
  66. package/src/preview/util.ts +1 -0
  67. package/src/project/project.test.ts +3 -3
  68. package/src/project/project.ts +28 -5
  69. package/src/projection/getProjectionState.test.ts +69 -49
  70. package/src/projection/getProjectionState.ts +42 -50
  71. package/src/projection/projectionQuery.ts +1 -1
  72. package/src/projection/projectionStore.test.ts +13 -51
  73. package/src/projection/projectionStore.ts +6 -18
  74. package/src/projection/resolveProjection.test.ts +32 -127
  75. package/src/projection/resolveProjection.ts +15 -28
  76. package/src/projection/subscribeToStateAndFetchBatches.test.ts +105 -90
  77. package/src/projection/subscribeToStateAndFetchBatches.ts +94 -81
  78. package/src/projection/util.ts +2 -0
  79. package/src/projects/projects.test.ts +13 -4
  80. package/src/projects/projects.ts +6 -1
  81. package/src/query/queryStore.test.ts +10 -47
  82. package/src/query/queryStore.ts +151 -133
  83. package/src/query/queryStoreConstants.ts +2 -0
  84. package/src/store/createActionBinder.test.ts +153 -0
  85. package/src/store/createActionBinder.ts +176 -0
  86. package/src/store/createSanityInstance.test.ts +84 -0
  87. package/src/store/createSanityInstance.ts +124 -0
  88. package/src/store/createStateSourceAction.test.ts +196 -0
  89. package/src/store/createStateSourceAction.ts +260 -0
  90. package/src/store/createStoreInstance.test.ts +81 -0
  91. package/src/store/createStoreInstance.ts +80 -0
  92. package/src/store/createStoreState.test.ts +85 -0
  93. package/src/store/createStoreState.ts +92 -0
  94. package/src/store/defineStore.test.ts +18 -0
  95. package/src/store/defineStore.ts +81 -0
  96. package/src/users/reducers.test.ts +318 -0
  97. package/src/users/reducers.ts +88 -0
  98. package/src/users/types.ts +46 -4
  99. package/src/users/usersConstants.ts +4 -0
  100. package/src/users/usersStore.test.ts +350 -223
  101. package/src/users/usersStore.ts +285 -149
  102. package/src/utils/createFetcherStore.test.ts +6 -7
  103. package/src/utils/createFetcherStore.ts +150 -153
  104. package/src/{common/util.test.ts → utils/hashString.test.ts} +1 -1
  105. package/src/auth/fetchLoginUrls.test.ts +0 -163
  106. package/src/auth/fetchLoginUrls.ts +0 -74
  107. package/src/common/createLiveEventSubscriber.test.ts +0 -121
  108. package/src/common/createLiveEventSubscriber.ts +0 -55
  109. package/src/common/types.ts +0 -4
  110. package/src/instance/identity.test.ts +0 -46
  111. package/src/instance/identity.ts +0 -29
  112. package/src/instance/sanityInstance.test.ts +0 -77
  113. package/src/instance/sanityInstance.ts +0 -57
  114. package/src/instance/types.ts +0 -37
  115. package/src/preview/getPreviewProjection.ts +0 -45
  116. package/src/resources/README.md +0 -370
  117. package/src/resources/createAction.test.ts +0 -101
  118. package/src/resources/createAction.ts +0 -44
  119. package/src/resources/createResource.test.ts +0 -112
  120. package/src/resources/createResource.ts +0 -102
  121. package/src/resources/createStateSourceAction.test.ts +0 -114
  122. package/src/resources/createStateSourceAction.ts +0 -83
  123. package/src/resources/createStore.test.ts +0 -67
  124. package/src/resources/createStore.ts +0 -46
  125. package/src/store/createStore.test.ts +0 -108
  126. package/src/store/createStore.ts +0 -106
  127. /package/src/{common/util.ts → utils/hashString.ts} +0 -0
@@ -0,0 +1,92 @@
1
+ import {Observable} from 'rxjs'
2
+ import {devtools, type DevtoolsOptions} from 'zustand/middleware'
3
+ import {createStore} from 'zustand/vanilla'
4
+
5
+ /**
6
+ * Represents a reactive store state container with multiple access patterns
7
+ */
8
+ export interface StoreState<TState> {
9
+ /**
10
+ * Gets the current state value
11
+ *
12
+ * @remarks
13
+ * This is a direct synchronous accessor that doesn't trigger subscriptions
14
+ */
15
+ get: () => TState
16
+
17
+ /**
18
+ * Updates the store state
19
+ * @param name - Action name for devtools tracking
20
+ * @param updatedState - New state value or updater function
21
+ *
22
+ * @remarks
23
+ * When providing a partial object, previous top-level keys not included in
24
+ * the update will be preserved.
25
+ */
26
+ set: (name: string, updatedState: Partial<TState> | ((s: TState) => Partial<TState>)) => void
27
+
28
+ /**
29
+ * Observable stream of state changes
30
+ * @remarks
31
+ * - Emits immediately with current state on subscription
32
+ * - Shares underlying subscription between observers
33
+ * - Only emits when state reference changes
34
+ * - Completes when store is disposed
35
+ */
36
+ observable: Observable<TState>
37
+ }
38
+
39
+ /**
40
+ * Creates a reactive store state container with multiple access patterns
41
+ * @param initialState - Initial state value for the store
42
+ * @param devToolsOptions - Configuration for Zustand devtools integration
43
+ * @returns StoreState instance with get/set/observable interface
44
+ *
45
+ * @example
46
+ * ```typescript
47
+ * // Create a simple counter store
48
+ * const counterStore = createStoreState({ count: 0 });
49
+ *
50
+ * // Update state
51
+ * counterStore.set('increment', { count: 1 });
52
+ *
53
+ * // Observe changes
54
+ * counterStore.observable.subscribe(console.log);
55
+ * ```
56
+ *
57
+ * @remarks
58
+ * Uses Zustand for state management under the hood with RxJS for observable interface.
59
+ * Designed to work with both imperative and reactive programming patterns.
60
+ */
61
+ export function createStoreState<TState>(
62
+ initialState: TState,
63
+ devToolsOptions?: DevtoolsOptions,
64
+ ): StoreState<TState> {
65
+ // Create underlying Zustand store with devtools integration
66
+ const store = createStore<TState>()(devtools(() => initialState, devToolsOptions))
67
+
68
+ return {
69
+ get: store.getState,
70
+ set: (actionKey, updatedState) => {
71
+ const currentState = store.getState()
72
+ const nextState =
73
+ typeof updatedState === 'function' ? updatedState(currentState) : updatedState
74
+
75
+ // Optimization: Skip update if state reference remains the same
76
+ if (currentState !== nextState) {
77
+ store.setState(nextState, false, actionKey)
78
+ }
79
+ },
80
+ observable: new Observable((observer) => {
81
+ // Emit current state immediately on subscription
82
+ const emit = () => observer.next(store.getState())
83
+ emit()
84
+
85
+ // Subscribe to Zustand store changes
86
+ const unsubscribe = store.subscribe(emit)
87
+
88
+ // Cleanup when observable unsubscribed
89
+ return () => unsubscribe()
90
+ }),
91
+ }
92
+ }
@@ -0,0 +1,18 @@
1
+ import {describe, expect, it} from 'vitest'
2
+
3
+ import {type SanityInstance} from './createSanityInstance'
4
+ import {defineStore, type StoreDefinition} from './defineStore'
5
+
6
+ describe('defineStore', () => {
7
+ it('should return the store definition unchanged', () => {
8
+ const storeDef: StoreDefinition<number> = {
9
+ name: 'TestStore',
10
+ getInitialState: () => 42,
11
+ }
12
+
13
+ const result = defineStore(storeDef)
14
+ expect(result).toBe(storeDef)
15
+ expect(result.name).toBe('TestStore')
16
+ expect(result.getInitialState({} as SanityInstance)).toBe(42)
17
+ })
18
+ })
@@ -0,0 +1,81 @@
1
+ import {type SanityInstance} from './createSanityInstance'
2
+ import {type StoreState} from './createStoreState'
3
+
4
+ /**
5
+ * Context object provided to store initialization functions
6
+ */
7
+ export interface StoreContext<TState> {
8
+ /**
9
+ * Sanity instance associated with this store
10
+ *
11
+ * @remarks
12
+ * Provides access to the Sanity configuration and instance lifecycle methods
13
+ */
14
+ instance: SanityInstance
15
+
16
+ /**
17
+ * Reactive store state management utilities
18
+ *
19
+ * @remarks
20
+ * Contains methods for getting/setting state and observing changes
21
+ */
22
+ state: StoreState<TState>
23
+ }
24
+
25
+ /**
26
+ * Defines the structure and behavior of a store
27
+ *
28
+ * @remarks
29
+ * Stores are isolated state containers that can be associated with Sanity instances.
30
+ * Each store definition creates a separate state instance per composite key.
31
+ */
32
+ export interface StoreDefinition<TState> {
33
+ /**
34
+ * Unique name for the store
35
+ *
36
+ * @remarks
37
+ * Used for debugging, devtools integration, and store identification
38
+ */
39
+ name: string
40
+
41
+ /**
42
+ * Creates the initial state for the store
43
+ * @param instance - Sanity instance the store is being created for
44
+ * @returns Initial state value
45
+ *
46
+ * @remarks
47
+ * Called when a new store instance is created. Can use Sanity instance
48
+ * configuration to determine initial state.
49
+ */
50
+ getInitialState: (instance: SanityInstance) => TState
51
+
52
+ /**
53
+ * Optional initialization function
54
+ * @param context - Store context with state and instance access
55
+ * @returns Optional cleanup function for store disposal
56
+ *
57
+ * @remarks
58
+ * Use this for:
59
+ * - Setting up event listeners
60
+ * - Initial data fetching
61
+ * - Connecting external services
62
+ *
63
+ * Return a cleanup function to:
64
+ * - Remove event listeners
65
+ * - Cancel pending operations
66
+ * - Dispose external connections
67
+ */
68
+ initialize?: (context: StoreContext<TState>) => (() => void) | undefined
69
+ }
70
+
71
+ /**
72
+ * Typescript helper function for creating store definitions
73
+ *
74
+ * @param storeDefinition - Configuration object defining the store
75
+ * @returns The finalized store definition
76
+ */
77
+ export function defineStore<TState>(
78
+ storeDefinition: StoreDefinition<TState>,
79
+ ): StoreDefinition<TState> {
80
+ return storeDefinition
81
+ }
@@ -0,0 +1,318 @@
1
+ import {describe, expect, it} from 'vitest'
2
+
3
+ import {type SanityInstance} from '../store/createSanityInstance'
4
+ import {
5
+ addSubscription,
6
+ cancelRequest,
7
+ getUsersKey,
8
+ initializeRequest,
9
+ parseUsersKey,
10
+ removeSubscription,
11
+ setUsersData,
12
+ setUsersError,
13
+ updateLastLoadMoreRequest,
14
+ } from './reducers'
15
+ import {type GetUsersOptions, type SanityUserResponse, type UsersStoreState} from './types'
16
+ import {DEFAULT_USERS_BATCH_SIZE} from './usersConstants'
17
+
18
+ describe('Users Reducers', () => {
19
+ // Mock SanityInstance for testing
20
+ const mockInstance: SanityInstance = {
21
+ instanceId: 'test-instance-id',
22
+ config: {
23
+ projectId: 'test-project-id',
24
+ },
25
+ isDisposed: () => false,
26
+ dispose: () => {},
27
+ onDispose: () => () => {},
28
+ getParent: () => undefined,
29
+ createChild: (_config) => mockInstance,
30
+ match: () => undefined,
31
+ }
32
+
33
+ const sampleOptions: GetUsersOptions = {
34
+ resourceType: 'project',
35
+ projectId: 'proj123',
36
+ batchSize: 50,
37
+ }
38
+
39
+ const sampleOptionsWithDefaultBatchSize: GetUsersOptions = {
40
+ resourceType: 'project',
41
+ projectId: 'proj123',
42
+ }
43
+
44
+ describe('getUsersKey', () => {
45
+ it('should generate a key based on resourceType, resourceId and batchSize', () => {
46
+ const key = getUsersKey(mockInstance, sampleOptions)
47
+ expect(JSON.parse(key)).toEqual({
48
+ resourceType: 'project',
49
+ projectId: 'proj123',
50
+ batchSize: 50,
51
+ })
52
+ })
53
+
54
+ it('should use DEFAULT_USERS_BATCH_SIZE when batchSize is not provided', () => {
55
+ const key = getUsersKey(mockInstance, sampleOptionsWithDefaultBatchSize)
56
+ expect(JSON.parse(key)).toEqual({
57
+ resourceType: 'project',
58
+ projectId: 'proj123',
59
+ batchSize: DEFAULT_USERS_BATCH_SIZE,
60
+ })
61
+ })
62
+ })
63
+
64
+ describe('parseUsersKey', () => {
65
+ it('should parse a key back into its components', () => {
66
+ const key = JSON.stringify({
67
+ resourceType: 'organization',
68
+ resourceId: 'org456',
69
+ batchSize: 25,
70
+ })
71
+ const parsed = parseUsersKey(key)
72
+ expect(parsed).toEqual({
73
+ resourceType: 'organization',
74
+ resourceId: 'org456',
75
+ batchSize: 25,
76
+ })
77
+ })
78
+ })
79
+
80
+ describe('addSubscription', () => {
81
+ it('should add a subscription when key does not exist', () => {
82
+ const state: UsersStoreState = {users: {}}
83
+ const key = getUsersKey(mockInstance, sampleOptions)
84
+ const reducer = addSubscription('sub1', key)
85
+ const newState = reducer(state)
86
+ expect(newState.users[key]).toEqual({subscriptions: ['sub1']})
87
+ })
88
+
89
+ it('should append subscription when key exists', () => {
90
+ const key = getUsersKey(mockInstance, sampleOptions)
91
+ const state: UsersStoreState = {users: {[key]: {subscriptions: ['subA']}}}
92
+ const reducer = addSubscription('subB', key)
93
+ const newState = reducer(state)
94
+ expect(newState.users[key]).toEqual({subscriptions: ['subA', 'subB']})
95
+ })
96
+ })
97
+
98
+ describe('removeSubscription', () => {
99
+ it('should return state unchanged if key does not exist', () => {
100
+ const state: UsersStoreState = {users: {}}
101
+ const key = getUsersKey(mockInstance, sampleOptions)
102
+ const reducer = removeSubscription('subX', key)
103
+ const newState = reducer(state)
104
+ expect(newState).toBe(state)
105
+ })
106
+
107
+ it('should remove subscription and keep the group if other subscriptions remain', () => {
108
+ const key = getUsersKey(mockInstance, sampleOptions)
109
+ const state: UsersStoreState = {users: {[key]: {subscriptions: ['sub1', 'sub2']}}}
110
+ const reducer = removeSubscription('sub1', key)
111
+ const newState = reducer(state)
112
+ expect(newState.users[key]).toEqual({subscriptions: ['sub2']})
113
+ })
114
+
115
+ it('should remove the group if no subscriptions remain after removal', () => {
116
+ const key = getUsersKey(mockInstance, sampleOptions)
117
+ const state: UsersStoreState = {users: {[key]: {subscriptions: ['onlySub']}}}
118
+ const reducer = removeSubscription('onlySub', key)
119
+ const newState = reducer(state)
120
+ expect(newState.users).not.toHaveProperty(key)
121
+ })
122
+ })
123
+
124
+ describe('setUsersData', () => {
125
+ it('should return state unchanged if key does not exist', () => {
126
+ const state: UsersStoreState = {users: {}}
127
+ const key = getUsersKey(mockInstance, sampleOptions)
128
+ const userResponse: SanityUserResponse = {
129
+ data: [
130
+ {
131
+ sanityUserId: 'user1',
132
+ profile: {
133
+ id: 'profile1',
134
+ displayName: 'User One',
135
+ email: 'user1@example.com',
136
+ provider: 'google',
137
+ createdAt: '2023-01-01',
138
+ },
139
+ memberships: [],
140
+ },
141
+ ],
142
+ totalCount: 1,
143
+ nextCursor: null,
144
+ }
145
+ const reducer = setUsersData(key, userResponse)
146
+ const newState = reducer(state)
147
+ expect(newState).toBe(state)
148
+ })
149
+
150
+ it('should set users data if key exists', () => {
151
+ const key = getUsersKey(mockInstance, sampleOptions)
152
+ const state: UsersStoreState = {users: {[key]: {subscriptions: ['sub1']}}}
153
+ const userResponse: SanityUserResponse = {
154
+ data: [
155
+ {
156
+ sanityUserId: 'user1',
157
+ profile: {
158
+ id: 'profile1',
159
+ displayName: 'User One',
160
+ email: 'user1@example.com',
161
+ provider: 'google',
162
+ createdAt: '2023-01-01',
163
+ },
164
+ memberships: [],
165
+ },
166
+ ],
167
+ totalCount: 1,
168
+ nextCursor: null,
169
+ }
170
+ const reducer = setUsersData(key, userResponse)
171
+ const newState = reducer(state)
172
+ expect(newState.users[key]).toEqual({
173
+ subscriptions: ['sub1'],
174
+ users: userResponse.data,
175
+ totalCount: 1,
176
+ nextCursor: null,
177
+ })
178
+ })
179
+
180
+ it('should append new users to existing users', () => {
181
+ const key = getUsersKey(mockInstance, sampleOptions)
182
+ const existingUser = {
183
+ sanityUserId: 'user1',
184
+ profile: {
185
+ id: 'profile1',
186
+ displayName: 'User One',
187
+ email: 'user1@example.com',
188
+ provider: 'google',
189
+ createdAt: '2023-01-01',
190
+ },
191
+ memberships: [],
192
+ }
193
+ const newUser = {
194
+ sanityUserId: 'user2',
195
+ profile: {
196
+ id: 'profile2',
197
+ displayName: 'User Two',
198
+ email: 'user2@example.com',
199
+ provider: 'google',
200
+ createdAt: '2023-01-02',
201
+ },
202
+ memberships: [],
203
+ }
204
+ const state: UsersStoreState = {
205
+ users: {
206
+ [key]: {
207
+ subscriptions: ['sub1'],
208
+ users: [existingUser],
209
+ },
210
+ },
211
+ }
212
+ const userResponse: SanityUserResponse = {
213
+ data: [newUser],
214
+ totalCount: 2,
215
+ nextCursor: null,
216
+ }
217
+ const reducer = setUsersData(key, userResponse)
218
+ const newState = reducer(state)
219
+ expect(newState.users[key]).toEqual({
220
+ subscriptions: ['sub1'],
221
+ users: [existingUser, newUser],
222
+ totalCount: 2,
223
+ nextCursor: null,
224
+ })
225
+ })
226
+ })
227
+
228
+ describe('updateLastLoadMoreRequest', () => {
229
+ it('should return state unchanged if key does not exist', () => {
230
+ const state: UsersStoreState = {users: {}}
231
+ const key = getUsersKey(mockInstance, sampleOptions)
232
+ const timestamp = '2023-05-15T12:00:00Z'
233
+ const reducer = updateLastLoadMoreRequest(timestamp, key)
234
+ const newState = reducer(state)
235
+ expect(newState).toBe(state)
236
+ })
237
+
238
+ it('should update lastLoadMoreRequest if key exists', () => {
239
+ const key = getUsersKey(mockInstance, sampleOptions)
240
+ const state: UsersStoreState = {users: {[key]: {subscriptions: ['sub1']}}}
241
+ const timestamp = '2023-05-15T12:00:00Z'
242
+ const reducer = updateLastLoadMoreRequest(timestamp, key)
243
+ const newState = reducer(state)
244
+ expect(newState.users[key]).toEqual({
245
+ subscriptions: ['sub1'],
246
+ lastLoadMoreRequest: timestamp,
247
+ })
248
+ })
249
+ })
250
+
251
+ describe('setUsersError', () => {
252
+ it('should return state unchanged if key does not exist', () => {
253
+ const state: UsersStoreState = {users: {}}
254
+ const key = getUsersKey(mockInstance, sampleOptions)
255
+ const error = new Error('Failed to fetch users')
256
+ const reducer = setUsersError(key, error)
257
+ const newState = reducer(state)
258
+ expect(newState).toBe(state)
259
+ })
260
+
261
+ it('should set error if key exists', () => {
262
+ const key = getUsersKey(mockInstance, sampleOptions)
263
+ const state: UsersStoreState = {users: {[key]: {subscriptions: ['sub1']}}}
264
+ const error = new Error('Failed to fetch users')
265
+ const reducer = setUsersError(key, error)
266
+ const newState = reducer(state)
267
+ expect(newState.users[key]).toEqual({
268
+ subscriptions: ['sub1'],
269
+ error,
270
+ })
271
+ })
272
+ })
273
+
274
+ describe('cancelRequest', () => {
275
+ it('should return state unchanged if key does not exist', () => {
276
+ const state: UsersStoreState = {users: {}}
277
+ const key = getUsersKey(mockInstance, sampleOptions)
278
+ const reducer = cancelRequest(key)
279
+ const newState = reducer(state)
280
+ expect(newState).toBe(state)
281
+ })
282
+
283
+ it('should return state unchanged if group has subscriptions', () => {
284
+ const key = getUsersKey(mockInstance, sampleOptions)
285
+ const state: UsersStoreState = {users: {[key]: {subscriptions: ['sub1']}}}
286
+ const reducer = cancelRequest(key)
287
+ const newState = reducer(state)
288
+ expect(newState).toBe(state)
289
+ })
290
+
291
+ it('should remove the group if no subscriptions exist', () => {
292
+ const key = getUsersKey(mockInstance, sampleOptions)
293
+ const state: UsersStoreState = {users: {[key]: {subscriptions: []}}}
294
+ const reducer = cancelRequest(key)
295
+ const newState = reducer(state)
296
+ expect(newState.users).not.toHaveProperty(key)
297
+ })
298
+ })
299
+
300
+ describe('initializeRequest', () => {
301
+ it('should return state unchanged if group already exists', () => {
302
+ const key = getUsersKey(mockInstance, sampleOptions)
303
+ const existing = {subscriptions: ['sub1']}
304
+ const state: UsersStoreState = {users: {[key]: existing}}
305
+ const reducer = initializeRequest(key)
306
+ const newState = reducer(state)
307
+ expect(newState).toBe(state)
308
+ })
309
+
310
+ it('should add the group with empty subscriptions if it does not exist', () => {
311
+ const state: UsersStoreState = {users: {}}
312
+ const key = getUsersKey(mockInstance, sampleOptions)
313
+ const reducer = initializeRequest(key)
314
+ const newState = reducer(state)
315
+ expect(newState.users[key]).toEqual({subscriptions: []})
316
+ })
317
+ })
318
+ })
@@ -0,0 +1,88 @@
1
+ import {omit} from 'lodash-es'
2
+
3
+ import {type SanityInstance} from '../store/createSanityInstance'
4
+ import {type GetUsersOptions, type SanityUserResponse, type UsersStoreState} from './types'
5
+ import {DEFAULT_USERS_BATCH_SIZE} from './usersConstants'
6
+
7
+ /** @internal */
8
+ export const getUsersKey = (
9
+ instance: SanityInstance,
10
+ {
11
+ resourceType,
12
+ organizationId,
13
+ batchSize = DEFAULT_USERS_BATCH_SIZE,
14
+ projectId = instance.config.projectId,
15
+ }: GetUsersOptions = {},
16
+ ): string =>
17
+ JSON.stringify({resourceType, organizationId, batchSize, projectId} satisfies ReturnType<
18
+ typeof parseUsersKey
19
+ >)
20
+
21
+ /** @internal */
22
+ export const parseUsersKey = (
23
+ key: string,
24
+ ): {
25
+ batchSize: number
26
+ resourceType?: 'organization' | 'project'
27
+ projectId?: string
28
+ organizationId?: string
29
+ } => JSON.parse(key)
30
+
31
+ export const addSubscription =
32
+ (subscriptionId: string, key: string) =>
33
+ (prev: UsersStoreState): UsersStoreState => {
34
+ const group = prev.users[key]
35
+ const subscriptions = [...(group?.subscriptions ?? []), subscriptionId]
36
+ return {...prev, users: {...prev.users, [key]: {...group, subscriptions}}}
37
+ }
38
+
39
+ export const removeSubscription =
40
+ (subscriptionId: string, key: string) =>
41
+ (prev: UsersStoreState): UsersStoreState => {
42
+ const group = prev.users[key]
43
+ if (!group) return prev
44
+ const subscriptions = group.subscriptions.filter((id) => id !== subscriptionId)
45
+ if (!subscriptions.length) return {...prev, users: omit(prev.users, key)}
46
+ return {...prev, users: {...prev.users, [key]: {...group, subscriptions}}}
47
+ }
48
+
49
+ export const setUsersData =
50
+ (key: string, {data, nextCursor, totalCount}: SanityUserResponse) =>
51
+ (prev: UsersStoreState): UsersStoreState => {
52
+ const group = prev.users[key]
53
+ if (!group) return prev
54
+ const users = [...(group.users ?? []), ...data]
55
+ return {...prev, users: {...prev.users, [key]: {...group, users, totalCount, nextCursor}}}
56
+ }
57
+
58
+ export const updateLastLoadMoreRequest =
59
+ (timestamp: string, key: string) =>
60
+ (prev: UsersStoreState): UsersStoreState => {
61
+ const group = prev.users[key]
62
+ if (!group) return prev
63
+ return {...prev, users: {...prev.users, [key]: {...group, lastLoadMoreRequest: timestamp}}}
64
+ }
65
+
66
+ export const setUsersError =
67
+ (key: string, error: unknown) =>
68
+ (prev: UsersStoreState): UsersStoreState => {
69
+ const group = prev.users[key]
70
+ if (!group) return prev
71
+ return {...prev, users: {...prev.users, [key]: {...group, error}}}
72
+ }
73
+
74
+ export const cancelRequest =
75
+ (key: string) =>
76
+ (prev: UsersStoreState): UsersStoreState => {
77
+ const group = prev.users[key]
78
+ if (!group) return prev
79
+ if (group.subscriptions.length) return prev
80
+ return {...prev, users: omit(prev.users, key)}
81
+ }
82
+
83
+ export const initializeRequest =
84
+ (key: string) =>
85
+ (prev: UsersStoreState): UsersStoreState => {
86
+ if (prev.users[key]) return prev
87
+ return {...prev, users: {...prev.users, [key]: {subscriptions: []}}}
88
+ }
@@ -1,7 +1,4 @@
1
- /**
2
- * @public
3
- */
4
- export type ResourceType = 'organization' | 'project'
1
+ import {type ProjectHandle} from '../config/sanityConfig'
5
2
 
6
3
  /**
7
4
  * @public
@@ -41,3 +38,48 @@ export interface UserProfile {
41
38
  isCurrentUser?: boolean
42
39
  providerId?: string
43
40
  }
41
+
42
+ /**
43
+ * @public
44
+ */
45
+ export interface GetUsersOptions extends ProjectHandle {
46
+ resourceType?: 'organization' | 'project'
47
+ batchSize?: number
48
+ organizationId?: string
49
+ }
50
+
51
+ /**
52
+ * @public
53
+ */
54
+ export interface UsersGroupState {
55
+ subscriptions: string[]
56
+ totalCount?: number
57
+ nextCursor?: string | null
58
+ lastLoadMoreRequest?: string
59
+ users?: SanityUser[]
60
+ error?: unknown
61
+ }
62
+
63
+ /**
64
+ * @public
65
+ */
66
+ export interface SanityUserResponse {
67
+ data: SanityUser[]
68
+ totalCount: number
69
+ nextCursor: string | null
70
+ }
71
+
72
+ /**
73
+ * @public
74
+ */
75
+ export interface UsersStoreState {
76
+ users: {[TUsersKey in string]?: UsersGroupState}
77
+ error?: unknown
78
+ }
79
+
80
+ /**
81
+ * @public
82
+ */
83
+ export interface ResolveUsersOptions extends GetUsersOptions {
84
+ signal?: AbortSignal
85
+ }
@@ -0,0 +1,4 @@
1
+ // NOTE: currently this API is only available on vX
2
+ export const API_VERSION = 'vX'
3
+ export const USERS_STATE_CLEAR_DELAY = 5000
4
+ export const DEFAULT_USERS_BATCH_SIZE = 100