@sanity/sdk 0.0.0-alpha.1
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 +339 -0
- package/dist/index.js +492 -0
- package/dist/index.js.map +1 -0
- package/package.json +77 -0
- package/src/_exports/index.ts +39 -0
- package/src/auth/authStore.test.ts +296 -0
- package/src/auth/authStore.ts +125 -0
- package/src/auth/getAuthStore.test.ts +14 -0
- package/src/auth/getInternalAuthStore.ts +20 -0
- package/src/auth/internalAuthStore.test.ts +334 -0
- package/src/auth/internalAuthStore.ts +519 -0
- package/src/client/getClient.test.ts +41 -0
- package/src/client/getClient.ts +13 -0
- package/src/client/getSubscribableClient.test.ts +71 -0
- package/src/client/getSubscribableClient.ts +17 -0
- package/src/client/store/actions/getClientEvents.test.ts +95 -0
- package/src/client/store/actions/getClientEvents.ts +33 -0
- package/src/client/store/actions/getOrCreateClient.test.ts +56 -0
- package/src/client/store/actions/getOrCreateClient.ts +40 -0
- package/src/client/store/actions/receiveToken.test.ts +18 -0
- package/src/client/store/actions/receiveToken.ts +31 -0
- package/src/client/store/clientStore.test.ts +152 -0
- package/src/client/store/clientStore.ts +98 -0
- package/src/documentList/documentListStore.test.ts +575 -0
- package/src/documentList/documentListStore.ts +269 -0
- package/src/documents/.keep +0 -0
- package/src/instance/identity.test.ts +46 -0
- package/src/instance/identity.ts +28 -0
- package/src/instance/sanityInstance.test.ts +66 -0
- package/src/instance/sanityInstance.ts +64 -0
- package/src/instance/types.d.ts +29 -0
- package/src/schema/schemaStore.test.ts +30 -0
- package/src/schema/schemaStore.ts +32 -0
- package/src/store/createStore.test.ts +108 -0
- package/src/store/createStore.ts +106 -0
- package/src/tsdoc.json +39 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import type {SyncTag} from '@sanity/client'
|
|
2
|
+
import {isEqual} from 'lodash-es'
|
|
3
|
+
import {
|
|
4
|
+
distinctUntilChanged,
|
|
5
|
+
map,
|
|
6
|
+
Observable,
|
|
7
|
+
pairwise,
|
|
8
|
+
startWith,
|
|
9
|
+
type Subscribable,
|
|
10
|
+
switchMap,
|
|
11
|
+
tap,
|
|
12
|
+
} from 'rxjs'
|
|
13
|
+
import {devtools} from 'zustand/middleware'
|
|
14
|
+
import {createStore} from 'zustand/vanilla'
|
|
15
|
+
|
|
16
|
+
import {getClientStore} from '../client/store/clientStore'
|
|
17
|
+
import type {SanityInstance} from '../instance/types'
|
|
18
|
+
|
|
19
|
+
const PAGE_SIZE = 50
|
|
20
|
+
|
|
21
|
+
const API_VERSION = 'vX'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Represents an identifier to a Sanity document, containing its `_id` to pull
|
|
25
|
+
* the document from content lake and its `_type` to look up its schema type.
|
|
26
|
+
* @public
|
|
27
|
+
*/
|
|
28
|
+
export interface DocumentHandle {
|
|
29
|
+
_id: string
|
|
30
|
+
_type: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Represents the current state of a document list, including the query options
|
|
35
|
+
* and loading status.
|
|
36
|
+
* @public
|
|
37
|
+
*/
|
|
38
|
+
export interface DocumentListState extends DocumentListOptions {
|
|
39
|
+
/** Array of document handles in the current result set, or null if not yet loaded */
|
|
40
|
+
result: DocumentHandle[] | null
|
|
41
|
+
/** Indicates whether the document list is currently loading */
|
|
42
|
+
isPending: boolean
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Represents a sort ordering configuration.
|
|
47
|
+
* @public
|
|
48
|
+
*/
|
|
49
|
+
export interface SortOrderingItem {
|
|
50
|
+
field: string
|
|
51
|
+
direction: 'asc' | 'desc'
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Configuration options for filtering and sorting documents in a document list.
|
|
56
|
+
* @public
|
|
57
|
+
*/
|
|
58
|
+
export interface DocumentListOptions {
|
|
59
|
+
/** GROQ filter expression to query specific documents */
|
|
60
|
+
filter?: string
|
|
61
|
+
/** Array of sort ordering specifications to determine the order of results */
|
|
62
|
+
sort?: SortOrderingItem[]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Manages the state and operations for a list of Sanity documents.
|
|
67
|
+
* Provides methods to update options, load more documents, and subscribe to
|
|
68
|
+
* state changes.
|
|
69
|
+
*
|
|
70
|
+
* Implements a subscription model where you can register callback functions
|
|
71
|
+
* to be notified when the document list state changes.
|
|
72
|
+
* @public
|
|
73
|
+
*/
|
|
74
|
+
export interface DocumentListStore extends Subscribable<DocumentListState> {
|
|
75
|
+
/** Updates the filtering and sorting options for the document list */
|
|
76
|
+
setOptions: (options: DocumentListOptions) => void
|
|
77
|
+
/** Retrieves the current state of the document list synchronously */
|
|
78
|
+
getCurrent: () => DocumentListState
|
|
79
|
+
/** Loads the next page of documents */
|
|
80
|
+
loadMore: () => void
|
|
81
|
+
/** Cleans up resources and subscriptions when the store is no longer needed */
|
|
82
|
+
dispose: () => void
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface DocumentListInternalState extends DocumentListOptions {
|
|
86
|
+
isPending: boolean
|
|
87
|
+
lastLiveEventId?: string
|
|
88
|
+
syncTags: Set<SyncTag>
|
|
89
|
+
limit: number
|
|
90
|
+
result: DocumentHandle[] | null
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Creates a `DocumentListStore` from a `SanityInstance`.
|
|
95
|
+
*
|
|
96
|
+
* @public
|
|
97
|
+
*
|
|
98
|
+
* See {@link SanityInstance} and {@link DocumentListStore}
|
|
99
|
+
*/
|
|
100
|
+
export function createDocumentListStore(instance: SanityInstance): DocumentListStore {
|
|
101
|
+
const clientStore = getClientStore(instance)
|
|
102
|
+
const clientStream$ = new Observable(
|
|
103
|
+
clientStore.getClientEvents({apiVersion: API_VERSION}).subscribe,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
const initialState: DocumentListInternalState = {
|
|
107
|
+
syncTags: new Set<SyncTag>(),
|
|
108
|
+
isPending: false,
|
|
109
|
+
result: null,
|
|
110
|
+
limit: PAGE_SIZE,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const store = createStore<DocumentListInternalState>()(
|
|
114
|
+
devtools((..._unusedArgs) => initialState, {
|
|
115
|
+
name: 'SanityDocumentListStore',
|
|
116
|
+
enabled: true, // Should be process.env.NODE_ENV === 'development'
|
|
117
|
+
}),
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
const liveSubscription = clientStream$
|
|
121
|
+
.pipe(
|
|
122
|
+
switchMap((client) => client.live.events({includeDrafts: true, tag: 'sdk.live-listener'})),
|
|
123
|
+
)
|
|
124
|
+
.subscribe({
|
|
125
|
+
next: (event) => {
|
|
126
|
+
const {syncTags} = store.getState()
|
|
127
|
+
if (event.type === 'message' && event.tags.some((tag) => syncTags.has(tag))) {
|
|
128
|
+
store.setState(
|
|
129
|
+
{lastLiveEventId: event.id},
|
|
130
|
+
false,
|
|
131
|
+
'UPDATE_EVENT_ID_FROM_LIVE_CONTENT_API',
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
const resultSubscription = new Observable<DocumentListInternalState>((observer) =>
|
|
138
|
+
store.subscribe((state) => observer.next(state)),
|
|
139
|
+
)
|
|
140
|
+
.pipe(
|
|
141
|
+
distinctUntilChanged((prev, current) => {
|
|
142
|
+
if (prev.filter !== current.filter) return false
|
|
143
|
+
if (prev.lastLiveEventId !== current.lastLiveEventId) return false
|
|
144
|
+
if (!isEqual(prev.sort, current.sort)) return false
|
|
145
|
+
if (prev.limit !== current.limit) return false
|
|
146
|
+
return true
|
|
147
|
+
}),
|
|
148
|
+
tap(() => store.setState({isPending: true}, false, {type: 'START_FETCH'})),
|
|
149
|
+
switchMap((state) => {
|
|
150
|
+
const filter = state.filter ? `[${state.filter}]` : ''
|
|
151
|
+
const order = state.sort
|
|
152
|
+
? `| order(${state.sort
|
|
153
|
+
.map((ordering) =>
|
|
154
|
+
[ordering.field, ordering.direction.toLowerCase()]
|
|
155
|
+
.map((str) => str.trim())
|
|
156
|
+
.filter(Boolean)
|
|
157
|
+
.join(' '),
|
|
158
|
+
)
|
|
159
|
+
.join(',')})`
|
|
160
|
+
: ''
|
|
161
|
+
|
|
162
|
+
return clientStream$.pipe(
|
|
163
|
+
switchMap((client) =>
|
|
164
|
+
client.observable.fetch(
|
|
165
|
+
`*${filter}${order}[0..$__limit]{_id, _type}`,
|
|
166
|
+
{__limit: state.limit},
|
|
167
|
+
{
|
|
168
|
+
filterResponse: false,
|
|
169
|
+
returnQuery: false,
|
|
170
|
+
lastLiveEventId: state.lastLiveEventId,
|
|
171
|
+
tag: 'sdk.document-list',
|
|
172
|
+
// // TODO: this should use the `previewDrafts` perspective for
|
|
173
|
+
// // removing duplicates in the result set but the live content API
|
|
174
|
+
// // does not currently return the correct sync tags. CLDX has
|
|
175
|
+
// // planned to add perspective support to the live content API
|
|
176
|
+
// // in december of 2024
|
|
177
|
+
// perspective: 'previewDrafts'
|
|
178
|
+
},
|
|
179
|
+
),
|
|
180
|
+
),
|
|
181
|
+
)
|
|
182
|
+
}),
|
|
183
|
+
)
|
|
184
|
+
.subscribe({
|
|
185
|
+
next: ({syncTags, result}) => {
|
|
186
|
+
store.setState(
|
|
187
|
+
{
|
|
188
|
+
syncTags: new Set(syncTags),
|
|
189
|
+
result,
|
|
190
|
+
isPending: false,
|
|
191
|
+
},
|
|
192
|
+
false,
|
|
193
|
+
{type: 'UPDATE_FROM_FETCH'},
|
|
194
|
+
)
|
|
195
|
+
},
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
function setOptions({filter, sort}: DocumentListOptions) {
|
|
199
|
+
store.setState(
|
|
200
|
+
{
|
|
201
|
+
// spreads properties only if they exist, preserving other state
|
|
202
|
+
// properties set in the zustand store already
|
|
203
|
+
...(filter && {filter}),
|
|
204
|
+
...(sort && {sort}),
|
|
205
|
+
},
|
|
206
|
+
false,
|
|
207
|
+
{type: 'SET_OPTIONS'},
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function loadMore() {
|
|
212
|
+
store.setState((prev) => ({limit: prev.limit + PAGE_SIZE}), undefined, {type: 'LOAD_MORE'})
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function getCurrent() {
|
|
216
|
+
const {isPending, result, filter, sort} = store.getState()
|
|
217
|
+
return {isPending, result, filter, sort}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const state$ = new Observable<DocumentListState>((observer) => {
|
|
221
|
+
function emitCurrent() {
|
|
222
|
+
const {isPending, result, filter, sort} = store.getState()
|
|
223
|
+
observer.next({
|
|
224
|
+
isPending,
|
|
225
|
+
result,
|
|
226
|
+
filter,
|
|
227
|
+
sort,
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
emitCurrent()
|
|
232
|
+
return store.subscribe(emitCurrent)
|
|
233
|
+
}).pipe(
|
|
234
|
+
distinctUntilChanged((prev, curr) => {
|
|
235
|
+
if (!isEqual(prev.sort, curr.sort)) return false
|
|
236
|
+
if (prev.result !== curr.result) return false
|
|
237
|
+
if (prev.filter !== curr.filter) return false
|
|
238
|
+
if (prev.isPending !== curr.isPending) return false
|
|
239
|
+
return true
|
|
240
|
+
}),
|
|
241
|
+
startWith(null),
|
|
242
|
+
pairwise(),
|
|
243
|
+
map((arg): DocumentListState => {
|
|
244
|
+
// casting this since `curr` will never be null
|
|
245
|
+
const [prev, curr] = arg as [DocumentListState | null, DocumentListState]
|
|
246
|
+
if (!prev?.result) return curr
|
|
247
|
+
if (!curr?.result) return curr
|
|
248
|
+
|
|
249
|
+
const prevMap = prev.result.reduce<Map<string, DocumentHandle>>((acc, handle) => {
|
|
250
|
+
acc.set(handle._id, handle)
|
|
251
|
+
return acc
|
|
252
|
+
}, new Map())
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
...curr,
|
|
256
|
+
result: curr.result.map((i) => prevMap.get(i._id) ?? i),
|
|
257
|
+
}
|
|
258
|
+
}),
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
const subscribe = state$.subscribe.bind(state$)
|
|
262
|
+
|
|
263
|
+
function dispose() {
|
|
264
|
+
liveSubscription.unsubscribe()
|
|
265
|
+
resultSubscription.unsubscribe()
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return {setOptions, getCurrent, loadMore, subscribe, dispose}
|
|
269
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {describe, expect, it} from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {getSdkIdentity} from './identity'
|
|
4
|
+
|
|
5
|
+
describe('identity', () => {
|
|
6
|
+
describe('getSdkIdentity', () => {
|
|
7
|
+
it('creates a frozen object with expected properties', () => {
|
|
8
|
+
const identity = getSdkIdentity({
|
|
9
|
+
projectId: 'test-project',
|
|
10
|
+
dataset: 'test-dataset',
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
// Check if object is frozen
|
|
14
|
+
expect(Object.isFrozen(identity)).toBe(true)
|
|
15
|
+
|
|
16
|
+
// Check if all expected properties exist
|
|
17
|
+
expect(identity).toHaveProperty('id')
|
|
18
|
+
expect(identity).toHaveProperty('projectId', 'test-project')
|
|
19
|
+
expect(identity).toHaveProperty('dataset', 'test-dataset')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('generates unique ids for different instances', () => {
|
|
23
|
+
const identity1 = getSdkIdentity({
|
|
24
|
+
projectId: 'test-project',
|
|
25
|
+
dataset: 'test-dataset',
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const identity2 = getSdkIdentity({
|
|
29
|
+
projectId: 'test-project',
|
|
30
|
+
dataset: 'test-dataset',
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
expect(identity1.id).not.toBe(identity2.id)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('generates id with correct format', () => {
|
|
37
|
+
const identity = getSdkIdentity({
|
|
38
|
+
projectId: 'test-project',
|
|
39
|
+
dataset: 'test-dataset',
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
// ID should be 16 characters long (8 pairs of hex digits)
|
|
43
|
+
expect(identity.id).toMatch(/^[0-9a-f]{16}$/)
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
})
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type {SdkIdentity} from './types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* thoughtLevel 2 - Primarily what we want is to have an object that we can bind stores/memoizations to, but let the things that depend on it (eg projectId, dataset) be internals that can't be overwritten by accident/intentionally
|
|
5
|
+
* @public
|
|
6
|
+
*/
|
|
7
|
+
export function getSdkIdentity({
|
|
8
|
+
projectId,
|
|
9
|
+
dataset,
|
|
10
|
+
}: {
|
|
11
|
+
projectId: string
|
|
12
|
+
dataset: string
|
|
13
|
+
}): SdkIdentity {
|
|
14
|
+
const id = generateId()
|
|
15
|
+
return Object.freeze({
|
|
16
|
+
id,
|
|
17
|
+
projectId,
|
|
18
|
+
dataset,
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function generateId() {
|
|
23
|
+
return Array.from({length: 8}, () =>
|
|
24
|
+
Math.floor(Math.random() * 16)
|
|
25
|
+
.toString(16)
|
|
26
|
+
.padStart(2, '0'),
|
|
27
|
+
).join('')
|
|
28
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import {beforeEach, describe, expect, test} from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {createSanityInstance, getOrCreateResource, type SanityConfig} from './sanityInstance'
|
|
4
|
+
|
|
5
|
+
describe('sanityInstance', () => {
|
|
6
|
+
let config: SanityConfig
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
config = {
|
|
10
|
+
projectId: 'test-project',
|
|
11
|
+
dataset: 'test-dataset',
|
|
12
|
+
}
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
describe('createSanityInstance', () => {
|
|
16
|
+
test('creates instance with correct configuration', () => {
|
|
17
|
+
const instance = createSanityInstance(config)
|
|
18
|
+
|
|
19
|
+
expect(instance.identity).toEqual(
|
|
20
|
+
expect.objectContaining({
|
|
21
|
+
projectId: 'test-project',
|
|
22
|
+
dataset: 'test-dataset',
|
|
23
|
+
}),
|
|
24
|
+
)
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
describe('getOrCreateResource', () => {
|
|
29
|
+
test('creates and caches resource', () => {
|
|
30
|
+
const instance = createSanityInstance(config)
|
|
31
|
+
let createCount = 0
|
|
32
|
+
|
|
33
|
+
const creator = () => {
|
|
34
|
+
createCount++
|
|
35
|
+
return {value: 'test-resource'}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// First call should create new resource
|
|
39
|
+
const resource1 = getOrCreateResource(instance, 'test-key', creator)
|
|
40
|
+
expect(resource1).toEqual({value: 'test-resource'})
|
|
41
|
+
expect(createCount).toBe(1)
|
|
42
|
+
|
|
43
|
+
// Second call should return cached resource
|
|
44
|
+
const resource2 = getOrCreateResource(instance, 'test-key', creator)
|
|
45
|
+
expect(resource2).toBe(resource1)
|
|
46
|
+
expect(createCount).toBe(1)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('different instances have separate resource caches', () => {
|
|
50
|
+
const instance1 = createSanityInstance({...config, projectId: 'project1'})
|
|
51
|
+
const instance2 = createSanityInstance({...config, projectId: 'project2'})
|
|
52
|
+
let createCount = 0
|
|
53
|
+
|
|
54
|
+
const creator = () => {
|
|
55
|
+
createCount++
|
|
56
|
+
return {value: 'test-resource'}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const resource1 = getOrCreateResource(instance1, 'test-key', creator)
|
|
60
|
+
const resource2 = getOrCreateResource(instance2, 'test-key', creator)
|
|
61
|
+
|
|
62
|
+
expect(resource1).not.toBe(resource2)
|
|
63
|
+
expect(createCount).toBe(2)
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
})
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type {AuthConfig} from '../auth/internalAuthStore'
|
|
2
|
+
import {getSdkIdentity} from './identity'
|
|
3
|
+
import type {SanityInstance, SdkIdentity} from './types'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @public
|
|
7
|
+
*/
|
|
8
|
+
export interface SanityConfig {
|
|
9
|
+
projectId: string
|
|
10
|
+
dataset: string
|
|
11
|
+
auth?: AuthConfig
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Returns a new instance of dependencies required for SanitySDK.
|
|
16
|
+
*
|
|
17
|
+
* @public
|
|
18
|
+
*
|
|
19
|
+
* @param config - The configuration for this instance
|
|
20
|
+
*
|
|
21
|
+
* @returns A new "instance" of a Sanity SDK, used to bind resources/configuration to it
|
|
22
|
+
*/
|
|
23
|
+
export function createSanityInstance({
|
|
24
|
+
projectId = '',
|
|
25
|
+
dataset = '',
|
|
26
|
+
...config
|
|
27
|
+
}: SanityConfig): SanityInstance {
|
|
28
|
+
return {
|
|
29
|
+
identity: getSdkIdentity({projectId, dataset}),
|
|
30
|
+
config,
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const resourceStorage = new WeakMap<SdkIdentity, Map<string, unknown>>()
|
|
35
|
+
|
|
36
|
+
function getResource(instance: SanityInstance, key: string) {
|
|
37
|
+
const instanceMap = resourceStorage.get(instance.identity)
|
|
38
|
+
return instanceMap?.get(key)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function setResource(instance: SanityInstance, key: string, value: unknown) {
|
|
42
|
+
let instanceMap = resourceStorage.get(instance.identity)
|
|
43
|
+
if (!instanceMap) {
|
|
44
|
+
instanceMap = new Map()
|
|
45
|
+
resourceStorage.set(instance.identity, instanceMap)
|
|
46
|
+
}
|
|
47
|
+
instanceMap.set(key, value)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* This is an internal function that retrieves or creates a Zustand store resource.
|
|
52
|
+
* @internal
|
|
53
|
+
*/
|
|
54
|
+
export function getOrCreateResource<T>(instance: SanityInstance, key: string, creator: () => T): T {
|
|
55
|
+
const cached = getResource(instance, key)
|
|
56
|
+
|
|
57
|
+
if (cached) {
|
|
58
|
+
return cached as T
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const resource = creator()
|
|
62
|
+
setResource(instance, key, resource)
|
|
63
|
+
return resource
|
|
64
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type {ClientStore} from '../client/store/clientStore'
|
|
2
|
+
import type {SchemaStore} from '../schema/schemaStore'
|
|
3
|
+
import type {SanityConfig} from './sanityInstance'
|
|
4
|
+
|
|
5
|
+
/** @internal */
|
|
6
|
+
export interface InternalStores {
|
|
7
|
+
clientStore?: ClientStore
|
|
8
|
+
schemaStore?: SchemaStore
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** @public */
|
|
12
|
+
export interface SanityInstance {
|
|
13
|
+
/**
|
|
14
|
+
* The following is used to look up resources associated with this instance,
|
|
15
|
+
* and can be used to retrieve an "id" for the instance - useful in debugging.
|
|
16
|
+
*
|
|
17
|
+
* @public
|
|
18
|
+
*/
|
|
19
|
+
readonly identity: SdkIdentity
|
|
20
|
+
|
|
21
|
+
config: Omit<SanityConfig, 'projectId' | 'dataset'>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** @public */
|
|
25
|
+
export interface SdkIdentity {
|
|
26
|
+
readonly id: string
|
|
27
|
+
readonly projectId: string
|
|
28
|
+
readonly dataset: string
|
|
29
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import {describe, expect, it} from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {createSchemaStore} from './schemaStore'
|
|
4
|
+
|
|
5
|
+
describe('schemaStore', () => {
|
|
6
|
+
it('should create a store with initial schema types', () => {
|
|
7
|
+
const mockTypes = [{name: 'post', type: 'document'}]
|
|
8
|
+
const store = createSchemaStore(mockTypes)
|
|
9
|
+
|
|
10
|
+
expect(store.getState().schema).toEqual({types: mockTypes})
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('should update schema when setSchema is called', () => {
|
|
14
|
+
const store = createSchemaStore([])
|
|
15
|
+
const newSchema = {types: [{name: 'author', type: 'document'}]}
|
|
16
|
+
|
|
17
|
+
store.getState().setSchema(newSchema)
|
|
18
|
+
|
|
19
|
+
expect(store.getState().schema).toEqual(newSchema)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('should maintain store reference when updating schema', () => {
|
|
23
|
+
const store = createSchemaStore([])
|
|
24
|
+
const initialStoreRef = store
|
|
25
|
+
|
|
26
|
+
store.getState().setSchema({types: []})
|
|
27
|
+
|
|
28
|
+
expect(store).toBe(initialStoreRef)
|
|
29
|
+
})
|
|
30
|
+
})
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import type {StoreApi} from 'zustand'
|
|
3
|
+
import {devtools} from 'zustand/middleware'
|
|
4
|
+
import {createStore} from 'zustand/vanilla'
|
|
5
|
+
|
|
6
|
+
/** @public */
|
|
7
|
+
export interface SchemaState {
|
|
8
|
+
schema: any
|
|
9
|
+
setSchema: (newSchema: any) => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** @public */
|
|
13
|
+
export type SchemaStore = StoreApi<SchemaState>
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* This is an internal function that creates a Zustand store for the schema.
|
|
17
|
+
* @internal
|
|
18
|
+
*/
|
|
19
|
+
export const createSchemaStore = (schemaTypes: any[]): SchemaStore => {
|
|
20
|
+
return createStore<SchemaState>()(
|
|
21
|
+
devtools(
|
|
22
|
+
(set) => ({
|
|
23
|
+
schema: {types: schemaTypes},
|
|
24
|
+
setSchema: (newSchema) => set({schema: newSchema}, false, 'setSchema'),
|
|
25
|
+
}),
|
|
26
|
+
{
|
|
27
|
+
name: 'SanitySchemaStore',
|
|
28
|
+
enabled: true, // Should be process.env.NODE_ENV === 'development'
|
|
29
|
+
},
|
|
30
|
+
),
|
|
31
|
+
)
|
|
32
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import {describe, expect, it, vi} from 'vitest'
|
|
2
|
+
|
|
3
|
+
import type {SanityInstance} from '../instance/types'
|
|
4
|
+
import {createStore, type StoreActionContext} from './createStore'
|
|
5
|
+
|
|
6
|
+
// Mock the devtools middleware
|
|
7
|
+
vi.mock('zustand/middleware', () => ({
|
|
8
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9
|
+
devtools: (storeFunction: any) => {
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11
|
+
return (...args: any[]) => storeFunction(...args)
|
|
12
|
+
},
|
|
13
|
+
}))
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
vi.clearAllMocks()
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
describe('createStore', () => {
|
|
20
|
+
// Setup mock instance
|
|
21
|
+
const mockInstance = {
|
|
22
|
+
identity: {
|
|
23
|
+
id: 'test-id',
|
|
24
|
+
},
|
|
25
|
+
} as SanityInstance
|
|
26
|
+
|
|
27
|
+
// Setup types for our test store
|
|
28
|
+
interface TestState {
|
|
29
|
+
count: number
|
|
30
|
+
text: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const initialState: TestState = {
|
|
34
|
+
count: 0,
|
|
35
|
+
text: '',
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Setup test actions
|
|
39
|
+
const createTestActions = () => ({
|
|
40
|
+
increment: ({store}: StoreActionContext<TestState>) => {
|
|
41
|
+
const state = store.getState()
|
|
42
|
+
store.setState({...state, count: state.count + 1})
|
|
43
|
+
},
|
|
44
|
+
setText: ({store}: StoreActionContext<TestState>, newText: string) => {
|
|
45
|
+
const state = store.getState()
|
|
46
|
+
store.setState({...state, text: newText})
|
|
47
|
+
return newText
|
|
48
|
+
},
|
|
49
|
+
getCount: ({store}: StoreActionContext<TestState>) => {
|
|
50
|
+
return store.getState().count
|
|
51
|
+
},
|
|
52
|
+
getText: ({store}: StoreActionContext<TestState>) => {
|
|
53
|
+
return store.getState().text
|
|
54
|
+
},
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('creates a store with initial state with all properties', () => {
|
|
58
|
+
const actions = createTestActions()
|
|
59
|
+
const store = createStore(initialState, actions, {
|
|
60
|
+
name: 'test-store',
|
|
61
|
+
instance: mockInstance,
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
// Verify store has all action methods
|
|
65
|
+
expect(store).toHaveProperty('increment')
|
|
66
|
+
expect(store).toHaveProperty('setText')
|
|
67
|
+
expect(store).toHaveProperty('getCount')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('maintains separate state for different stores', () => {
|
|
71
|
+
const actions = createTestActions()
|
|
72
|
+
const store1 = createStore(initialState, actions, {
|
|
73
|
+
name: 'store-1',
|
|
74
|
+
instance: mockInstance,
|
|
75
|
+
})
|
|
76
|
+
const store2 = createStore(initialState, actions, {
|
|
77
|
+
name: 'store-2',
|
|
78
|
+
instance: mockInstance,
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
store1.increment()
|
|
82
|
+
store2.setText('hello')
|
|
83
|
+
|
|
84
|
+
// Verify stores are unaffected
|
|
85
|
+
expect(store1.getCount()).toBe(1)
|
|
86
|
+
expect(store2.getCount()).toBe(0)
|
|
87
|
+
|
|
88
|
+
expect(store1.getText()).toBe('')
|
|
89
|
+
expect(store2.getText()).toBe('hello')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('correctly updates state through actions', () => {
|
|
93
|
+
const actions = createTestActions()
|
|
94
|
+
const store = createStore(initialState, actions, {
|
|
95
|
+
name: 'test-store',
|
|
96
|
+
instance: mockInstance,
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
store.increment()
|
|
100
|
+
expect(store.getCount()).toBe(1)
|
|
101
|
+
|
|
102
|
+
store.increment()
|
|
103
|
+
expect(store.getCount()).toBe(2)
|
|
104
|
+
|
|
105
|
+
store.setText('hello world')
|
|
106
|
+
expect(store.getText()).toBe('hello world')
|
|
107
|
+
})
|
|
108
|
+
})
|