@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,153 @@
|
|
|
1
|
+
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {bindActionByDataset, bindActionGlobally, createActionBinder} from './createActionBinder'
|
|
4
|
+
import {createSanityInstance} from './createSanityInstance'
|
|
5
|
+
import {createStoreInstance} from './createStoreInstance'
|
|
6
|
+
|
|
7
|
+
// Mock store instance creation for testing
|
|
8
|
+
vi.mock('./createStoreInstance', () => ({
|
|
9
|
+
createStoreInstance: vi.fn(() => ({state: {counter: 0}, dispose: vi.fn()})),
|
|
10
|
+
}))
|
|
11
|
+
beforeEach(() => vi.mocked(createStoreInstance).mockClear())
|
|
12
|
+
|
|
13
|
+
describe('createActionBinder', () => {
|
|
14
|
+
it('should bind an action and call it with correct context and parameters, using caching', () => {
|
|
15
|
+
const binder = createActionBinder(() => '')
|
|
16
|
+
const storeDefinition = {
|
|
17
|
+
name: 'TestStore',
|
|
18
|
+
getInitialState: () => ({counter: 0}),
|
|
19
|
+
}
|
|
20
|
+
// Action that increments counter by given value
|
|
21
|
+
const action = vi.fn((context, increment: number) => {
|
|
22
|
+
context.state.counter += increment
|
|
23
|
+
return context.state.counter
|
|
24
|
+
})
|
|
25
|
+
const boundAction = binder(storeDefinition, action)
|
|
26
|
+
const instance = createSanityInstance({projectId: 'proj1', dataset: 'ds1'})
|
|
27
|
+
|
|
28
|
+
// First call creates store instance
|
|
29
|
+
const result1 = boundAction(instance, 5)
|
|
30
|
+
expect(result1).toBe(5)
|
|
31
|
+
// Second call reuses cached store
|
|
32
|
+
const result2 = boundAction(instance, 5)
|
|
33
|
+
expect(result2).toBe(10)
|
|
34
|
+
|
|
35
|
+
expect(action).toHaveBeenCalledTimes(2)
|
|
36
|
+
expect(vi.mocked(createStoreInstance)).toHaveBeenCalledTimes(1)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('should create separate store instances for different composite keys', () => {
|
|
40
|
+
const binder = createActionBinder(({projectId, dataset}) => `${projectId}.${dataset}`)
|
|
41
|
+
const storeDefinition = {
|
|
42
|
+
name: 'TestStore',
|
|
43
|
+
getInitialState: () => ({counter: 0}),
|
|
44
|
+
}
|
|
45
|
+
const action = vi.fn((context, val: number) => {
|
|
46
|
+
context.state.counter += val
|
|
47
|
+
return context.state.counter
|
|
48
|
+
})
|
|
49
|
+
const boundAction = binder(storeDefinition, action)
|
|
50
|
+
const instanceA = createSanityInstance({projectId: 'p1', dataset: 'd1'})
|
|
51
|
+
const instanceB = createSanityInstance({projectId: 'p2', dataset: 'd2'})
|
|
52
|
+
|
|
53
|
+
const resultA = boundAction(instanceA, 3)
|
|
54
|
+
const resultB = boundAction(instanceB, 4)
|
|
55
|
+
|
|
56
|
+
expect(resultA).toBe(3)
|
|
57
|
+
expect(resultB).toBe(4)
|
|
58
|
+
expect(vi.mocked(createStoreInstance)).toHaveBeenCalledTimes(2)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('should dispose the store instance when the last instance is disposed', () => {
|
|
62
|
+
const binder = createActionBinder(() => '')
|
|
63
|
+
const storeDefinition = {
|
|
64
|
+
name: 'TestStore',
|
|
65
|
+
getInitialState: () => ({counter: 0}),
|
|
66
|
+
}
|
|
67
|
+
const action = vi.fn((context) => context.state.counter)
|
|
68
|
+
const boundAction = binder(storeDefinition, action)
|
|
69
|
+
const instance1 = createSanityInstance({projectId: 'p1', dataset: 'd1'})
|
|
70
|
+
const instance2 = createSanityInstance({projectId: 'p1', dataset: 'd1'})
|
|
71
|
+
|
|
72
|
+
// Call action on both instances
|
|
73
|
+
boundAction(instance1)
|
|
74
|
+
boundAction(instance2)
|
|
75
|
+
expect(vi.mocked(createStoreInstance)).toHaveBeenCalledTimes(1)
|
|
76
|
+
|
|
77
|
+
const [{value: storeInstance}] = vi.mocked(createStoreInstance).mock.results
|
|
78
|
+
expect(storeInstance).toBeDefined()
|
|
79
|
+
|
|
80
|
+
// First disposal shouldn't trigger store disposal
|
|
81
|
+
instance1.dispose()
|
|
82
|
+
expect(storeInstance.dispose).not.toHaveBeenCalled()
|
|
83
|
+
|
|
84
|
+
// Last disposal should trigger store disposal
|
|
85
|
+
instance2.dispose()
|
|
86
|
+
expect(storeInstance.dispose).toHaveBeenCalledTimes(1)
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
describe('bindActionByDataset', () => {
|
|
91
|
+
it('should work correctly when projectId and dataset are provided', () => {
|
|
92
|
+
const storeDefinition = {
|
|
93
|
+
name: 'DSStore',
|
|
94
|
+
getInitialState: () => ({counter: 0}),
|
|
95
|
+
}
|
|
96
|
+
const action = vi.fn((_context, value: string) => value)
|
|
97
|
+
const boundAction = bindActionByDataset(storeDefinition, action)
|
|
98
|
+
const instance = createSanityInstance({projectId: 'proj1', dataset: 'ds1'})
|
|
99
|
+
const result = boundAction(instance, 'hello')
|
|
100
|
+
expect(result).toBe('hello')
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('should throw an error if projectId or dataset is missing', () => {
|
|
104
|
+
const storeDefinition = {
|
|
105
|
+
name: 'DSStore',
|
|
106
|
+
getInitialState: () => ({counter: 0}),
|
|
107
|
+
}
|
|
108
|
+
const action = vi.fn((_context) => 'fail')
|
|
109
|
+
const boundAction = bindActionByDataset(storeDefinition, action)
|
|
110
|
+
// Instance with missing dataset
|
|
111
|
+
const instance = createSanityInstance({projectId: 'proj1', dataset: ''})
|
|
112
|
+
expect(() => boundAction(instance)).toThrow(
|
|
113
|
+
'This API requires a project ID and dataset configured.',
|
|
114
|
+
)
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
describe('bindActionGlobally', () => {
|
|
119
|
+
it('should work correctly ignoring config in key generation', () => {
|
|
120
|
+
const storeDefinition = {
|
|
121
|
+
name: 'GlobalStore',
|
|
122
|
+
getInitialState: () => ({counter: 0}),
|
|
123
|
+
}
|
|
124
|
+
const action = vi.fn((_context, x: number) => x)
|
|
125
|
+
const boundAction = bindActionGlobally(storeDefinition, action)
|
|
126
|
+
|
|
127
|
+
// Create instances with different configs
|
|
128
|
+
const instance1 = createSanityInstance({projectId: 'any', dataset: 'any'})
|
|
129
|
+
const instance2 = createSanityInstance({projectId: 'different', dataset: 'config'})
|
|
130
|
+
|
|
131
|
+
// Both instances should use the same store
|
|
132
|
+
const result1 = boundAction(instance1, 42)
|
|
133
|
+
const result2 = boundAction(instance2, 99)
|
|
134
|
+
|
|
135
|
+
expect(result1).toBe(42)
|
|
136
|
+
expect(result2).toBe(99)
|
|
137
|
+
|
|
138
|
+
// Verify single store instance used
|
|
139
|
+
expect(vi.mocked(createStoreInstance)).toHaveBeenCalledTimes(1)
|
|
140
|
+
|
|
141
|
+
// Verify action called with correct arguments
|
|
142
|
+
expect(action).toHaveBeenNthCalledWith(1, expect.anything(), 42)
|
|
143
|
+
expect(action).toHaveBeenNthCalledWith(2, expect.anything(), 99)
|
|
144
|
+
|
|
145
|
+
// Test disposal tracking
|
|
146
|
+
const [{value: storeInstance}] = vi.mocked(createStoreInstance).mock.results
|
|
147
|
+
instance1.dispose()
|
|
148
|
+
expect(storeInstance.dispose).not.toHaveBeenCalled()
|
|
149
|
+
|
|
150
|
+
instance2.dispose()
|
|
151
|
+
expect(storeInstance.dispose).toHaveBeenCalledTimes(1)
|
|
152
|
+
})
|
|
153
|
+
})
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import {type SanityConfig} from '../config/sanityConfig'
|
|
2
|
+
import {type SanityInstance} from './createSanityInstance'
|
|
3
|
+
import {createStoreInstance, type StoreInstance} from './createStoreInstance'
|
|
4
|
+
import {type StoreState} from './createStoreState'
|
|
5
|
+
import {type StoreContext, type StoreDefinition} from './defineStore'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Defines a store action that operates on a specific state type
|
|
9
|
+
*/
|
|
10
|
+
export type StoreAction<TState, TParams extends unknown[], TReturn> = (
|
|
11
|
+
context: StoreContext<TState>,
|
|
12
|
+
...params: TParams
|
|
13
|
+
) => TReturn
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Represents a store action that has been bound to a specific store instance
|
|
17
|
+
*/
|
|
18
|
+
export type BoundStoreAction<_TState, TParams extends unknown[], TReturn> = (
|
|
19
|
+
instance: SanityInstance,
|
|
20
|
+
...params: TParams
|
|
21
|
+
) => TReturn
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Creates an action binder function that uses the provided key function
|
|
25
|
+
* to determine how store instances are shared between Sanity instances
|
|
26
|
+
*
|
|
27
|
+
* @param keyFn - Function that generates a key from a Sanity config
|
|
28
|
+
* @returns A function that binds store actions to Sanity instances
|
|
29
|
+
*
|
|
30
|
+
* @remarks
|
|
31
|
+
* Action binders determine how store instances are shared across multiple
|
|
32
|
+
* Sanity instances. The key function determines which instances share state.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* // Create a custom binder that uses a tenant ID for isolation
|
|
37
|
+
* const bindActionByTenant = createActionBinder(config => config.tenantId || 'default')
|
|
38
|
+
*
|
|
39
|
+
* // Use the custom binder with a store definition
|
|
40
|
+
* const getTenantUsers = bindActionByTenant(
|
|
41
|
+
* userStore,
|
|
42
|
+
* ({state}) => state.get().users
|
|
43
|
+
* )
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export function createActionBinder(keyFn: (config: SanityConfig) => string) {
|
|
47
|
+
const instanceRegistry = new Map<string, Set<string>>()
|
|
48
|
+
const storeRegistry = new Map<string, StoreInstance<unknown>>()
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Binds a store action to a store definition
|
|
52
|
+
*
|
|
53
|
+
* @param storeDefinition - The store definition
|
|
54
|
+
* @param action - The action to bind
|
|
55
|
+
* @returns A function that executes the action with a Sanity instance
|
|
56
|
+
*/
|
|
57
|
+
return function bindAction<TState, TParams extends unknown[], TReturn>(
|
|
58
|
+
storeDefinition: StoreDefinition<TState>,
|
|
59
|
+
action: StoreAction<TState, TParams, TReturn>,
|
|
60
|
+
): BoundStoreAction<TState, TParams, TReturn> {
|
|
61
|
+
return function boundAction(instance: SanityInstance, ...params: TParams) {
|
|
62
|
+
const keySuffix = keyFn(instance.config)
|
|
63
|
+
const compositeKey = storeDefinition.name + (keySuffix ? `:${keySuffix}` : '')
|
|
64
|
+
|
|
65
|
+
// Get or create instance set for this composite key
|
|
66
|
+
let instances = instanceRegistry.get(compositeKey)
|
|
67
|
+
if (!instances) {
|
|
68
|
+
instances = new Set<string>()
|
|
69
|
+
instanceRegistry.set(compositeKey, instances)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Register instance for disposal tracking
|
|
73
|
+
if (!instances.has(instance.instanceId)) {
|
|
74
|
+
instances.add(instance.instanceId)
|
|
75
|
+
instance.onDispose(() => {
|
|
76
|
+
instances.delete(instance.instanceId)
|
|
77
|
+
|
|
78
|
+
// Clean up when last instance is disposed
|
|
79
|
+
if (instances.size === 0) {
|
|
80
|
+
storeRegistry.get(compositeKey)?.dispose()
|
|
81
|
+
storeRegistry.delete(compositeKey)
|
|
82
|
+
instanceRegistry.delete(compositeKey)
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Get or create store instance
|
|
88
|
+
let storeInstance = storeRegistry.get(compositeKey)
|
|
89
|
+
if (!storeInstance) {
|
|
90
|
+
storeInstance = createStoreInstance(instance, storeDefinition)
|
|
91
|
+
storeRegistry.set(compositeKey, storeInstance)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Execute action with store context
|
|
95
|
+
return action({instance, state: storeInstance.state as StoreState<TState>}, ...params)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Binds an action to a store that's scoped to a specific project and dataset
|
|
102
|
+
*
|
|
103
|
+
* @remarks
|
|
104
|
+
* This creates actions that operate on state isolated to a specific projectId and dataset.
|
|
105
|
+
* Different project/dataset combinations will have separate states.
|
|
106
|
+
*
|
|
107
|
+
* @throws Error if projectId or dataset is missing from the Sanity instance config
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* ```ts
|
|
111
|
+
* // Define a store
|
|
112
|
+
* const documentStore = defineStore<DocumentState>({
|
|
113
|
+
* name: 'Document',
|
|
114
|
+
* getInitialState: () => ({ documents: {} }),
|
|
115
|
+
* // ...
|
|
116
|
+
* })
|
|
117
|
+
*
|
|
118
|
+
* // Create dataset-specific actions
|
|
119
|
+
* export const fetchDocument = bindActionByDataset(
|
|
120
|
+
* documentStore,
|
|
121
|
+
* ({instance, state}, documentId) => {
|
|
122
|
+
* // This state is isolated to the specific project/dataset
|
|
123
|
+
* // ...fetch logic...
|
|
124
|
+
* }
|
|
125
|
+
* )
|
|
126
|
+
*
|
|
127
|
+
* // Usage
|
|
128
|
+
* fetchDocument(sanityInstance, 'doc123')
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
export const bindActionByDataset = createActionBinder(({projectId, dataset}) => {
|
|
132
|
+
if (!projectId || !dataset) {
|
|
133
|
+
throw new Error('This API requires a project ID and dataset configured.')
|
|
134
|
+
}
|
|
135
|
+
return `${projectId}.${dataset}`
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Binds an action to a global store that's shared across all Sanity instances
|
|
140
|
+
*
|
|
141
|
+
* @remarks
|
|
142
|
+
* This creates actions that operate on state shared globally across all Sanity instances.
|
|
143
|
+
* Use this for features like authentication where the state should be the same
|
|
144
|
+
* regardless of which project or dataset is being used.
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* ```ts
|
|
148
|
+
* // Define a store
|
|
149
|
+
* const authStore = defineStore<AuthState>({
|
|
150
|
+
* name: 'Auth',
|
|
151
|
+
* getInitialState: () => ({
|
|
152
|
+
* user: null,
|
|
153
|
+
* isAuthenticated: false
|
|
154
|
+
* }),
|
|
155
|
+
* // ...
|
|
156
|
+
* })
|
|
157
|
+
*
|
|
158
|
+
* // Create global actions
|
|
159
|
+
* export const getCurrentUser = bindActionGlobally(
|
|
160
|
+
* authStore,
|
|
161
|
+
* ({state}) => state.get().user
|
|
162
|
+
* )
|
|
163
|
+
*
|
|
164
|
+
* export const login = bindActionGlobally(
|
|
165
|
+
* authStore,
|
|
166
|
+
* ({state, instance}, credentials) => {
|
|
167
|
+
* // Login logic that affects global state
|
|
168
|
+
* // ...
|
|
169
|
+
* }
|
|
170
|
+
* )
|
|
171
|
+
*
|
|
172
|
+
* // Usage with any instance
|
|
173
|
+
* getCurrentUser(sanityInstance)
|
|
174
|
+
* ```
|
|
175
|
+
*/
|
|
176
|
+
export const bindActionGlobally = createActionBinder(() => 'global')
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import {describe, expect, it, vi} from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {createSanityInstance} from './createSanityInstance'
|
|
4
|
+
|
|
5
|
+
describe('createSanityInstance', () => {
|
|
6
|
+
it('should create an instance with a unique instanceId and given config', () => {
|
|
7
|
+
const instance = createSanityInstance({projectId: 'proj1', dataset: 'ds1'})
|
|
8
|
+
expect(typeof instance.instanceId).toBe('string')
|
|
9
|
+
expect(instance.config).toEqual({projectId: 'proj1', dataset: 'ds1'})
|
|
10
|
+
expect(instance.isDisposed()).toBe(false)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('should dispose an instance and call onDispose callbacks', () => {
|
|
14
|
+
const instance = createSanityInstance({projectId: 'proj1', dataset: 'ds1'})
|
|
15
|
+
const callback = vi.fn()
|
|
16
|
+
instance.onDispose(callback)
|
|
17
|
+
instance.dispose()
|
|
18
|
+
expect(instance.isDisposed()).toBe(true)
|
|
19
|
+
expect(callback).toHaveBeenCalled()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('should not call onDispose callbacks more than once when disposed multiple times', () => {
|
|
23
|
+
const instance = createSanityInstance({projectId: 'proj1', dataset: 'ds1'})
|
|
24
|
+
const callback = vi.fn()
|
|
25
|
+
instance.onDispose(callback)
|
|
26
|
+
instance.dispose()
|
|
27
|
+
instance.dispose()
|
|
28
|
+
expect(callback).toHaveBeenCalledTimes(1)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('should create a child instance with merged config and correct parent', () => {
|
|
32
|
+
const parent = createSanityInstance({projectId: 'proj1', dataset: 'ds1'})
|
|
33
|
+
const child = parent.createChild({dataset: 'ds2'})
|
|
34
|
+
expect(child.config).toEqual({projectId: 'proj1', dataset: 'ds2'})
|
|
35
|
+
expect(child.getParent()).toBe(parent)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('should match an instance in the hierarchy using match', () => {
|
|
39
|
+
// three-level hierarchy
|
|
40
|
+
const grandparent = createSanityInstance({projectId: 'proj1', dataset: 'ds1'})
|
|
41
|
+
const parent = grandparent.createChild({projectId: 'proj2'})
|
|
42
|
+
const child = parent.createChild({dataset: 'ds2'})
|
|
43
|
+
|
|
44
|
+
expect(child.config).toEqual({projectId: 'proj2', dataset: 'ds2'})
|
|
45
|
+
expect(parent.config).toEqual({projectId: 'proj2', dataset: 'ds1'})
|
|
46
|
+
|
|
47
|
+
expect(child.match({dataset: 'ds2'})).toBe(child)
|
|
48
|
+
expect(child.match({projectId: 'proj2'})).toBe(child)
|
|
49
|
+
expect(child.match({projectId: 'proj1'})).toBe(grandparent)
|
|
50
|
+
expect(parent.match({projectId: 'proj1'})).toBe(grandparent)
|
|
51
|
+
expect(grandparent.match({projectId: 'proj1'})).toBe(grandparent)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('should match `undefined` when the desired resource ID should not be set on an instance', () => {
|
|
55
|
+
const noProjectOrDataset = createSanityInstance()
|
|
56
|
+
const noDataset = noProjectOrDataset.createChild({projectId: 'proj1'})
|
|
57
|
+
const leaf = noDataset.createChild({dataset: 'ds1'})
|
|
58
|
+
|
|
59
|
+
// no keys means anything (in this case, self) will match
|
|
60
|
+
expect(leaf.match({})).toBe(leaf)
|
|
61
|
+
|
|
62
|
+
// `[resourceId]: undefined` means match an instance with no dataset set
|
|
63
|
+
expect(leaf.match({dataset: undefined})).toBe(noDataset)
|
|
64
|
+
expect(noDataset.match({dataset: undefined})).toBe(noDataset)
|
|
65
|
+
expect(leaf.match({projectId: undefined})).toBe(noProjectOrDataset)
|
|
66
|
+
expect(noDataset.match({projectId: undefined})).toBe(noProjectOrDataset)
|
|
67
|
+
expect(noProjectOrDataset.match({projectId: undefined})).toBe(noProjectOrDataset)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('should return undefined when no match is found', () => {
|
|
71
|
+
const instance = createSanityInstance({projectId: 'proj1', dataset: 'ds1'})
|
|
72
|
+
expect(instance.match({dataset: 'non-existent'})).toBeUndefined()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('should inherit and merge auth config', () => {
|
|
76
|
+
const parent = createSanityInstance({
|
|
77
|
+
projectId: 'proj1',
|
|
78
|
+
dataset: 'ds1',
|
|
79
|
+
auth: {apiHost: 'api.sanity.work'},
|
|
80
|
+
})
|
|
81
|
+
const child = parent.createChild({auth: {token: 'my-token'}})
|
|
82
|
+
expect(child.config.auth).toEqual({apiHost: 'api.sanity.work', token: 'my-token'})
|
|
83
|
+
})
|
|
84
|
+
})
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import {pick} from 'lodash-es'
|
|
2
|
+
|
|
3
|
+
import {type SanityConfig} from '../config/sanityConfig'
|
|
4
|
+
import {insecureRandomId} from '../utils/ids'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Represents a Sanity.io resource instance with its own configuration and lifecycle
|
|
8
|
+
* @remarks Instances form a hierarchy through parent/child relationships
|
|
9
|
+
*
|
|
10
|
+
* @public
|
|
11
|
+
*/
|
|
12
|
+
export interface SanityInstance {
|
|
13
|
+
/**
|
|
14
|
+
* Unique identifier for this instance
|
|
15
|
+
* @remarks Generated using crypto.randomUUID()
|
|
16
|
+
*/
|
|
17
|
+
readonly instanceId: string
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Resolved configuration for this instance
|
|
21
|
+
* @remarks Merges values from parent instances where appropriate
|
|
22
|
+
*/
|
|
23
|
+
readonly config: SanityConfig
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Checks if the instance has been disposed
|
|
27
|
+
* @returns true if dispose() has been called
|
|
28
|
+
*/
|
|
29
|
+
isDisposed(): boolean
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Disposes the instance and cleans up associated resources
|
|
33
|
+
* @remarks Triggers all registered onDispose callbacks
|
|
34
|
+
*/
|
|
35
|
+
dispose(): void
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Registers a callback to be invoked when the instance is disposed
|
|
39
|
+
* @param cb - Callback to execute on disposal
|
|
40
|
+
* @returns Function to unsubscribe the callback
|
|
41
|
+
*/
|
|
42
|
+
onDispose(cb: () => void): () => void
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Gets the parent instance in the hierarchy
|
|
46
|
+
* @returns Parent instance or undefined if this is the root
|
|
47
|
+
*/
|
|
48
|
+
getParent(): SanityInstance | undefined
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Creates a child instance with merged configuration
|
|
52
|
+
* @param config - Configuration to merge with parent values
|
|
53
|
+
* @remarks Child instances inherit parent configuration but can override values
|
|
54
|
+
*/
|
|
55
|
+
createChild(config: SanityConfig): SanityInstance
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Traverses the instance hierarchy to find the first instance whose configuration
|
|
59
|
+
* matches the given target config using a shallow comparison.
|
|
60
|
+
* @param targetConfig - A partial configuration object containing key-value pairs to match.
|
|
61
|
+
* @returns The first matching instance or undefined if no match is found.
|
|
62
|
+
*/
|
|
63
|
+
match(targetConfig: Partial<SanityConfig>): SanityInstance | undefined
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Creates a new Sanity resource instance
|
|
68
|
+
* @param config - Configuration for the instance (optional)
|
|
69
|
+
* @returns A configured SanityInstance
|
|
70
|
+
* @remarks When creating child instances, configurations are merged with parent values
|
|
71
|
+
*
|
|
72
|
+
* @public
|
|
73
|
+
*/
|
|
74
|
+
export function createSanityInstance(config: SanityConfig = {}): SanityInstance {
|
|
75
|
+
const instanceId = crypto.randomUUID()
|
|
76
|
+
const disposeListeners = new Map<string, () => void>()
|
|
77
|
+
const disposed = {current: false}
|
|
78
|
+
|
|
79
|
+
const instance: SanityInstance = {
|
|
80
|
+
instanceId,
|
|
81
|
+
config,
|
|
82
|
+
isDisposed: () => disposed.current,
|
|
83
|
+
dispose: () => {
|
|
84
|
+
if (disposed.current) return
|
|
85
|
+
disposed.current = true
|
|
86
|
+
disposeListeners.forEach((listener) => listener())
|
|
87
|
+
disposeListeners.clear()
|
|
88
|
+
},
|
|
89
|
+
onDispose: (cb) => {
|
|
90
|
+
const listenerId = insecureRandomId()
|
|
91
|
+
disposeListeners.set(listenerId, cb)
|
|
92
|
+
return () => {
|
|
93
|
+
disposeListeners.delete(listenerId)
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
getParent: () => undefined,
|
|
97
|
+
createChild: (next) =>
|
|
98
|
+
Object.assign(
|
|
99
|
+
createSanityInstance({
|
|
100
|
+
...config,
|
|
101
|
+
...next,
|
|
102
|
+
...(config.auth === next.auth
|
|
103
|
+
? config.auth
|
|
104
|
+
: config.auth && next.auth && {auth: {...config.auth, ...next.auth}}),
|
|
105
|
+
}),
|
|
106
|
+
{getParent: () => instance},
|
|
107
|
+
),
|
|
108
|
+
match: (targetConfig) => {
|
|
109
|
+
if (
|
|
110
|
+
Object.entries(pick(targetConfig, 'auth', 'projectId', 'dataset')).every(
|
|
111
|
+
([key, value]) => config[key as keyof SanityConfig] === value,
|
|
112
|
+
)
|
|
113
|
+
) {
|
|
114
|
+
return instance
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const parent = instance.getParent()
|
|
118
|
+
if (parent) return parent.match(targetConfig)
|
|
119
|
+
return undefined
|
|
120
|
+
},
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return instance
|
|
124
|
+
}
|