@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
|
@@ -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
|
+
}
|
package/src/users/types.ts
CHANGED
|
@@ -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
|
+
}
|