@sanity/sdk 2.6.0 → 2.7.0
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 +124 -13
- package/dist/index.js +468 -243
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
- package/src/_exports/index.ts +3 -0
- package/src/auth/authMode.test.ts +56 -0
- package/src/auth/authMode.ts +71 -0
- package/src/auth/authStore.test.ts +85 -4
- package/src/auth/authStore.ts +63 -125
- package/src/auth/authStrategy.ts +39 -0
- package/src/auth/dashboardAuth.ts +132 -0
- package/src/auth/standaloneAuth.ts +109 -0
- package/src/auth/studioAuth.ts +217 -0
- package/src/auth/studioModeAuth.test.ts +43 -1
- package/src/auth/studioModeAuth.ts +10 -1
- package/src/auth/subscribeToStateAndFetchCurrentUser.ts +21 -6
- package/src/config/sanityConfig.ts +48 -7
- package/src/projection/getProjectionState.ts +6 -5
- package/src/projection/projectionQuery.test.ts +38 -55
- package/src/projection/projectionQuery.ts +27 -31
- package/src/projection/projectionStore.test.ts +4 -4
- package/src/projection/projectionStore.ts +3 -2
- package/src/projection/resolveProjection.ts +2 -2
- package/src/projection/statusQuery.test.ts +35 -0
- package/src/projection/statusQuery.ts +71 -0
- package/src/projection/subscribeToStateAndFetchBatches.test.ts +63 -50
- package/src/projection/subscribeToStateAndFetchBatches.ts +106 -27
- package/src/projection/types.ts +12 -0
- package/src/projection/util.ts +0 -1
- package/src/query/queryStore.test.ts +64 -0
- package/src/query/queryStore.ts +30 -10
- package/src/releases/getPerspectiveState.test.ts +17 -14
- package/src/releases/getPerspectiveState.ts +58 -38
- package/src/releases/releasesStore.test.ts +59 -61
- package/src/releases/releasesStore.ts +21 -35
- package/src/releases/utils/isReleasePerspective.ts +7 -0
- package/src/store/createActionBinder.test.ts +211 -1
- package/src/store/createActionBinder.ts +95 -17
- package/src/store/createSanityInstance.ts +3 -1
|
@@ -15,13 +15,18 @@ vi.mock('../query/queryStore')
|
|
|
15
15
|
describe('subscribeToStateAndFetchBatches', () => {
|
|
16
16
|
let instance: SanityInstance
|
|
17
17
|
let state: StoreState<ProjectionStoreState>
|
|
18
|
-
const key = {
|
|
18
|
+
const key = {
|
|
19
|
+
name: 'test.test:drafts',
|
|
20
|
+
source: {projectId: 'test', dataset: 'test'},
|
|
21
|
+
perspective: 'drafts' as const,
|
|
22
|
+
}
|
|
19
23
|
|
|
20
24
|
beforeEach(() => {
|
|
21
25
|
vi.clearAllMocks()
|
|
22
26
|
instance = createSanityInstance({projectId: 'test', dataset: 'test'})
|
|
23
27
|
state = createStoreState<ProjectionStoreState>({
|
|
24
28
|
documentProjections: {},
|
|
29
|
+
documentStatuses: {},
|
|
25
30
|
subscriptions: {},
|
|
26
31
|
values: {},
|
|
27
32
|
})
|
|
@@ -65,19 +70,14 @@ describe('subscribeToStateAndFetchBatches', () => {
|
|
|
65
70
|
// Wait for debounce
|
|
66
71
|
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
67
72
|
|
|
68
|
-
//
|
|
69
|
-
expect(getQueryState).toHaveBeenCalledTimes(
|
|
73
|
+
// only 2 calls (one for projection, one for status) even though we added 2 subscriptions
|
|
74
|
+
expect(getQueryState).toHaveBeenCalledTimes(2)
|
|
70
75
|
expect(getQueryState).toHaveBeenCalledWith(
|
|
71
76
|
instance,
|
|
72
77
|
expect.objectContaining({
|
|
73
|
-
|
|
78
|
+
perspective: 'drafts',
|
|
74
79
|
params: {
|
|
75
|
-
[`__ids_${projectionHash}`]: expect.arrayContaining([
|
|
76
|
-
'doc1',
|
|
77
|
-
'drafts.doc1',
|
|
78
|
-
'doc2',
|
|
79
|
-
'drafts.doc2',
|
|
80
|
-
]),
|
|
80
|
+
[`__ids_${projectionHash}`]: expect.arrayContaining(['doc1', 'doc2']),
|
|
81
81
|
},
|
|
82
82
|
}),
|
|
83
83
|
)
|
|
@@ -87,47 +87,43 @@ describe('subscribeToStateAndFetchBatches', () => {
|
|
|
87
87
|
|
|
88
88
|
it('processes query results and updates state with resolved values', async () => {
|
|
89
89
|
const teardown = vi.fn()
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
observable
|
|
97
|
-
|
|
90
|
+
const projectionObservers: Observer<ProjectionQueryResult[] | undefined>[] = []
|
|
91
|
+
const statusObservers: Observer<{_id: string; _updatedAt: string}[] | undefined>[] = []
|
|
92
|
+
|
|
93
|
+
vi.mocked(getQueryState).mockImplementation((_, options) => {
|
|
94
|
+
const isStatusQuery = options.perspective === 'raw'
|
|
95
|
+
const observers = isStatusQuery ? statusObservers : projectionObservers
|
|
96
|
+
const observable = new Observable<unknown>((observer) => {
|
|
97
|
+
observers.push(observer as Observer<ProjectionQueryResult[] | undefined>)
|
|
98
|
+
return teardown
|
|
99
|
+
})
|
|
100
|
+
return {
|
|
101
|
+
getCurrent: () => undefined,
|
|
102
|
+
observable,
|
|
103
|
+
} as StateSource<ProjectionQueryResult[] | undefined>
|
|
104
|
+
})
|
|
98
105
|
|
|
99
106
|
const subscription = subscribeToStateAndFetchBatches({instance, state, key})
|
|
100
107
|
const projection = '{title}'
|
|
101
108
|
const projectionHash = hashString(projection)
|
|
102
109
|
|
|
103
|
-
expect(subscriber).not.toHaveBeenCalled()
|
|
104
|
-
|
|
105
110
|
// Add a subscription
|
|
106
111
|
state.set('addSubscription', {
|
|
107
112
|
documentProjections: {doc1: {[projectionHash]: projection}},
|
|
108
113
|
subscriptions: {doc1: {[projectionHash]: {sub1: true}}},
|
|
109
114
|
})
|
|
110
115
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
// Wait for debounce
|
|
116
|
+
// Wait for debounce (combineLatest subscribes to both projection and status)
|
|
114
117
|
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
115
118
|
|
|
116
|
-
expect(
|
|
119
|
+
expect(projectionObservers.length).toBe(1)
|
|
120
|
+
expect(statusObservers.length).toBe(1)
|
|
117
121
|
expect(teardown).not.toHaveBeenCalled()
|
|
118
122
|
|
|
119
|
-
const [observer] = subscriber.mock.lastCall!
|
|
120
|
-
|
|
121
123
|
const timestamp = new Date().toISOString()
|
|
122
124
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
_id: 'doc1',
|
|
126
|
-
_type: 'doc',
|
|
127
|
-
_updatedAt: timestamp,
|
|
128
|
-
result: {title: 'resolved'},
|
|
129
|
-
__projectionHash: projectionHash,
|
|
130
|
-
},
|
|
125
|
+
// Emit projection results first
|
|
126
|
+
projectionObservers[0]!.next([
|
|
131
127
|
{
|
|
132
128
|
_id: 'drafts.doc1',
|
|
133
129
|
_type: 'doc',
|
|
@@ -137,6 +133,12 @@ describe('subscribeToStateAndFetchBatches', () => {
|
|
|
137
133
|
},
|
|
138
134
|
])
|
|
139
135
|
|
|
136
|
+
// Emit status results (raw query returns _id, _updatedAt per document variant)
|
|
137
|
+
statusObservers[0]!.next([
|
|
138
|
+
{_id: 'doc1', _updatedAt: timestamp},
|
|
139
|
+
{_id: 'drafts.doc1', _updatedAt: timestamp},
|
|
140
|
+
])
|
|
141
|
+
|
|
140
142
|
const {values} = state.get()
|
|
141
143
|
expect(values['doc1']?.[projectionHash]).toEqual({
|
|
142
144
|
isPending: false,
|
|
@@ -247,11 +249,9 @@ describe('subscribeToStateAndFetchBatches', () => {
|
|
|
247
249
|
|
|
248
250
|
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
249
251
|
|
|
250
|
-
// Expected calls:
|
|
251
|
-
//
|
|
252
|
-
|
|
253
|
-
// 3. Debounced fetch for (doc1, hash1) AND (doc2, hash2)
|
|
254
|
-
expect(getQueryState).toHaveBeenCalledTimes(initialQueryCallCount + 1)
|
|
252
|
+
// Expected calls: initial batch has 2 (projection + status). After adding doc2,
|
|
253
|
+
// debounced fetch triggers 2 more (projection + status for new batch).
|
|
254
|
+
expect(getQueryState).toHaveBeenCalledTimes(initialQueryCallCount + 2)
|
|
255
255
|
// Abort should have been called because the required projections changed
|
|
256
256
|
expect(abortSpy).toHaveBeenCalled()
|
|
257
257
|
|
|
@@ -259,13 +259,21 @@ describe('subscribeToStateAndFetchBatches', () => {
|
|
|
259
259
|
})
|
|
260
260
|
|
|
261
261
|
it('processes and applies fetch results correctly', async () => {
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
vi.mocked(getQueryState).
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
262
|
+
const projectionObservers: Observer<ProjectionQueryResult[] | undefined>[] = []
|
|
263
|
+
const statusObservers: Observer<{_id: string; _updatedAt: string}[] | undefined>[] = []
|
|
264
|
+
|
|
265
|
+
vi.mocked(getQueryState).mockImplementation((_, options) => {
|
|
266
|
+
const isStatusQuery = options.perspective === 'raw'
|
|
267
|
+
const observers = isStatusQuery ? statusObservers : projectionObservers
|
|
268
|
+
const observable = new Observable<unknown>((observer) => {
|
|
269
|
+
observers.push(observer as Observer<ProjectionQueryResult[] | undefined>)
|
|
270
|
+
return () => {}
|
|
271
|
+
})
|
|
272
|
+
return {
|
|
273
|
+
getCurrent: () => undefined,
|
|
274
|
+
observable,
|
|
275
|
+
} as StateSource<ProjectionQueryResult[] | undefined>
|
|
276
|
+
})
|
|
269
277
|
|
|
270
278
|
const subscription = subscribeToStateAndFetchBatches({instance, state, key})
|
|
271
279
|
const projection = '{title, description}'
|
|
@@ -280,12 +288,12 @@ describe('subscribeToStateAndFetchBatches', () => {
|
|
|
280
288
|
|
|
281
289
|
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
282
290
|
|
|
283
|
-
expect(
|
|
284
|
-
|
|
291
|
+
expect(projectionObservers.length).toBe(1)
|
|
292
|
+
expect(statusObservers.length).toBe(1)
|
|
285
293
|
|
|
286
294
|
// Emit fetch results
|
|
287
295
|
const timestamp = '2024-01-01T00:00:00Z'
|
|
288
|
-
|
|
296
|
+
projectionObservers[0]!.next([
|
|
289
297
|
{
|
|
290
298
|
_id: 'doc1',
|
|
291
299
|
_type: 'test',
|
|
@@ -294,13 +302,18 @@ describe('subscribeToStateAndFetchBatches', () => {
|
|
|
294
302
|
__projectionHash: projectionHash,
|
|
295
303
|
},
|
|
296
304
|
])
|
|
305
|
+
statusObservers[0]!.next([
|
|
306
|
+
{_id: 'doc1', _updatedAt: timestamp},
|
|
307
|
+
{_id: 'drafts.doc1', _updatedAt: timestamp},
|
|
308
|
+
])
|
|
297
309
|
|
|
298
|
-
// Check that the state was updated
|
|
310
|
+
// Check that the state was updated (status query provides both draft and published _updatedAt)
|
|
299
311
|
expect(state.get().values['doc1']?.[projectionHash]).toEqual({
|
|
300
312
|
data: expect.objectContaining({
|
|
301
313
|
title: 'Test Document',
|
|
302
314
|
description: 'Test Description',
|
|
303
315
|
_status: {
|
|
316
|
+
lastEditedDraftAt: timestamp,
|
|
304
317
|
lastEditedPublishedAt: timestamp,
|
|
305
318
|
},
|
|
306
319
|
}),
|
|
@@ -16,27 +16,34 @@ import {
|
|
|
16
16
|
tap,
|
|
17
17
|
} from 'rxjs'
|
|
18
18
|
|
|
19
|
+
import {isDatasetSource} from '../config/sanityConfig'
|
|
19
20
|
import {getQueryState, resolveQuery} from '../query/queryStore'
|
|
20
|
-
import {type
|
|
21
|
+
import {type BoundPerspectiveKey} from '../store/createActionBinder'
|
|
21
22
|
import {type StoreContext} from '../store/defineStore'
|
|
22
23
|
import {
|
|
23
24
|
createProjectionQuery,
|
|
24
25
|
processProjectionQuery,
|
|
25
26
|
type ProjectionQueryResult,
|
|
26
27
|
} from './projectionQuery'
|
|
28
|
+
import {buildStatusQueryIds, processStatusQueryResults} from './statusQuery'
|
|
27
29
|
import {type ProjectionStoreState} from './types'
|
|
28
|
-
import {
|
|
30
|
+
import {PROJECTION_TAG} from './util'
|
|
29
31
|
|
|
30
32
|
const BATCH_DEBOUNCE_TIME = 50
|
|
31
33
|
|
|
32
34
|
const isSetEqual = <T>(a: Set<T>, b: Set<T>) =>
|
|
33
35
|
a.size === b.size && Array.from(a).every((i) => b.has(i))
|
|
34
36
|
|
|
37
|
+
interface StatusQueryResult {
|
|
38
|
+
_id: string
|
|
39
|
+
_updatedAt: string
|
|
40
|
+
}
|
|
41
|
+
|
|
35
42
|
export const subscribeToStateAndFetchBatches = ({
|
|
36
43
|
state,
|
|
37
44
|
instance,
|
|
38
|
-
key: {
|
|
39
|
-
}: StoreContext<ProjectionStoreState,
|
|
45
|
+
key: {source, perspective},
|
|
46
|
+
}: StoreContext<ProjectionStoreState, BoundPerspectiveKey>): Subscription => {
|
|
40
47
|
const documentProjections$ = state.observable.pipe(
|
|
41
48
|
map((s) => s.documentProjections),
|
|
42
49
|
distinctUntilChanged(isEqual),
|
|
@@ -92,34 +99,43 @@ export const subscribeToStateAndFetchBatches = ({
|
|
|
92
99
|
const {query, params} = createProjectionQuery(ids, documentProjections)
|
|
93
100
|
const controller = new AbortController()
|
|
94
101
|
|
|
95
|
-
|
|
102
|
+
// Build status query IDs and query
|
|
103
|
+
const statusQueryIds = buildStatusQueryIds(ids, perspective)
|
|
104
|
+
const statusQuery = `*[_id in $statusIds]{_id, _updatedAt}`
|
|
105
|
+
const statusParams = {statusIds: statusQueryIds}
|
|
106
|
+
|
|
107
|
+
const projectionQuery$ = new Observable<ProjectionQueryResult[]>((observer) => {
|
|
96
108
|
const {getCurrent, observable} = getQueryState<ProjectionQueryResult[]>(instance, {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
109
|
+
...{
|
|
110
|
+
query,
|
|
111
|
+
params,
|
|
112
|
+
tag: PROJECTION_TAG,
|
|
113
|
+
perspective,
|
|
114
|
+
},
|
|
115
|
+
// temporary guard here until we're ready for everything to be queried via global API
|
|
116
|
+
...(source && !isDatasetSource(source) ? {source} : {}),
|
|
103
117
|
})
|
|
104
118
|
|
|
105
|
-
const
|
|
119
|
+
const querySource$ = defer(() => {
|
|
106
120
|
if (getCurrent() === undefined) {
|
|
107
121
|
return from(
|
|
108
122
|
resolveQuery<ProjectionQueryResult[]>(instance, {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
123
|
+
...{
|
|
124
|
+
query,
|
|
125
|
+
params,
|
|
126
|
+
tag: PROJECTION_TAG,
|
|
127
|
+
signal: controller.signal,
|
|
128
|
+
perspective,
|
|
129
|
+
},
|
|
130
|
+
// temporary guard here until we're ready for everything to be queried via global API in v3
|
|
131
|
+
...(source && !isDatasetSource(source) ? {source} : {}),
|
|
116
132
|
}),
|
|
117
133
|
).pipe(switchMap(() => observable))
|
|
118
134
|
}
|
|
119
135
|
return observable
|
|
120
136
|
}).pipe(filter((result): result is ProjectionQueryResult[] => result !== undefined))
|
|
121
137
|
|
|
122
|
-
const subscription =
|
|
138
|
+
const subscription = querySource$.subscribe(observer)
|
|
123
139
|
|
|
124
140
|
return () => {
|
|
125
141
|
if (!controller.signal.aborted) {
|
|
@@ -127,16 +143,79 @@ export const subscribeToStateAndFetchBatches = ({
|
|
|
127
143
|
}
|
|
128
144
|
subscription.unsubscribe()
|
|
129
145
|
}
|
|
130
|
-
})
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
const statusQuery$ = new Observable<StatusQueryResult[]>((observer) => {
|
|
149
|
+
const {getCurrent, observable} = getQueryState<StatusQueryResult[]>(instance, {
|
|
150
|
+
...{
|
|
151
|
+
query: statusQuery,
|
|
152
|
+
params: statusParams,
|
|
153
|
+
tag: PROJECTION_TAG,
|
|
154
|
+
perspective: 'raw',
|
|
155
|
+
},
|
|
156
|
+
// temporary guard here until we're ready for everything to be queried via global API
|
|
157
|
+
...(source && !isDatasetSource(source) ? {source} : {}),
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
const statusQuerySource$ = defer(() => {
|
|
161
|
+
if (getCurrent() === undefined) {
|
|
162
|
+
return from(
|
|
163
|
+
resolveQuery<StatusQueryResult[]>(instance, {
|
|
164
|
+
...{
|
|
165
|
+
query: statusQuery,
|
|
166
|
+
params: statusParams,
|
|
167
|
+
tag: PROJECTION_TAG,
|
|
168
|
+
signal: controller.signal,
|
|
169
|
+
perspective: 'raw',
|
|
170
|
+
},
|
|
171
|
+
// temporary guard here until we're ready for everything to be queried via global API
|
|
172
|
+
...(source && !isDatasetSource(source) ? {source} : {}),
|
|
173
|
+
}),
|
|
174
|
+
).pipe(switchMap(() => observable))
|
|
175
|
+
}
|
|
176
|
+
return observable
|
|
177
|
+
}).pipe(filter((result): result is StatusQueryResult[] => result !== undefined))
|
|
178
|
+
|
|
179
|
+
const subscription = statusQuerySource$.subscribe(observer)
|
|
180
|
+
|
|
181
|
+
return () => {
|
|
182
|
+
subscription.unsubscribe()
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
// Combine both streams: emit whenever either has a new value (after both have
|
|
187
|
+
// emitted at least one defined value). This keeps reacting to query store updates.
|
|
188
|
+
return combineLatest([projectionQuery$, statusQuery$]).pipe(
|
|
189
|
+
filter(
|
|
190
|
+
(pair): pair is [ProjectionQueryResult[], StatusQueryResult[]] =>
|
|
191
|
+
pair[0] !== undefined && pair[1] !== undefined,
|
|
192
|
+
),
|
|
193
|
+
map(([projection, status]) => ({
|
|
194
|
+
data: projection,
|
|
195
|
+
ids,
|
|
196
|
+
statusResults: status,
|
|
197
|
+
})),
|
|
198
|
+
)
|
|
131
199
|
}),
|
|
132
|
-
map(({ids, data}) =>
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
200
|
+
map(({ids, data, statusResults}) => {
|
|
201
|
+
// Process status results into documentStatuses (same shape as _status returned to users)
|
|
202
|
+
const documentStatuses = processStatusQueryResults(statusResults)
|
|
203
|
+
state.set('updateStatuses', (prev) => ({
|
|
204
|
+
documentStatuses: {
|
|
205
|
+
...prev.documentStatuses,
|
|
206
|
+
...documentStatuses,
|
|
207
|
+
},
|
|
208
|
+
}))
|
|
209
|
+
|
|
210
|
+
// Assemble projection values with _status (buildStatusForDocument in statusQuery)
|
|
211
|
+
const currentState = state.get()
|
|
212
|
+
return processProjectionQuery({
|
|
136
213
|
ids,
|
|
137
214
|
results: data,
|
|
138
|
-
|
|
139
|
-
|
|
215
|
+
documentStatuses: currentState.documentStatuses,
|
|
216
|
+
perspective: perspective,
|
|
217
|
+
})
|
|
218
|
+
}),
|
|
140
219
|
)
|
|
141
220
|
.subscribe({
|
|
142
221
|
next: (processedValues) => {
|
package/src/projection/types.ts
CHANGED
|
@@ -29,6 +29,11 @@ interface DocumentProjectionSubscriptions {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
interface DocumentStatus {
|
|
33
|
+
lastEditedDraftAt?: string
|
|
34
|
+
lastEditedPublishedAt?: string
|
|
35
|
+
lastEditedVersionAt?: string
|
|
36
|
+
}
|
|
32
37
|
export interface ProjectionStoreState<TValue extends object = object> {
|
|
33
38
|
/**
|
|
34
39
|
* A map of document IDs to their projection values, organized by projection hash
|
|
@@ -50,4 +55,11 @@ export interface ProjectionStoreState<TValue extends object = object> {
|
|
|
50
55
|
subscriptions: {
|
|
51
56
|
[documentId: string]: DocumentProjectionSubscriptions
|
|
52
57
|
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A map of document IDs to their status information (same shape as _status returned to users)
|
|
61
|
+
*/
|
|
62
|
+
documentStatuses: {
|
|
63
|
+
[documentId: string]: DocumentStatus
|
|
64
|
+
}
|
|
53
65
|
}
|
package/src/projection/util.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {delay, filter, firstValueFrom, Observable, of, Subject} from 'rxjs'
|
|
|
3
3
|
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
4
4
|
|
|
5
5
|
import {getClientState} from '../client/clientStore'
|
|
6
|
+
import {isCanvasSource} from '../config/sanityConfig'
|
|
6
7
|
import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
|
|
7
8
|
import {type StateSource} from '../store/createStateSourceAction'
|
|
8
9
|
import {getQueryState, resolveQuery} from './queryStore'
|
|
@@ -16,6 +17,23 @@ vi.mock('../client/clientStore', () => ({
|
|
|
16
17
|
getClientState: vi.fn(),
|
|
17
18
|
}))
|
|
18
19
|
|
|
20
|
+
// Avoid initializing releases store/perspectives in these tests to prevent
|
|
21
|
+
// side-effect queries (e.g. releases::all()) that would duplicate fetch calls
|
|
22
|
+
vi.mock('../releases/getPerspectiveState', async () => {
|
|
23
|
+
const actual = await vi.importActual('../releases/getPerspectiveState')
|
|
24
|
+
return {
|
|
25
|
+
...actual,
|
|
26
|
+
getPerspectiveState: vi.fn(
|
|
27
|
+
(_instance, options?: {perspective?: unknown}) =>
|
|
28
|
+
({
|
|
29
|
+
subscribe: () => () => {},
|
|
30
|
+
getCurrent: () => (options?.perspective ?? 'drafts') as unknown,
|
|
31
|
+
observable: of((options?.perspective ?? 'drafts') as unknown),
|
|
32
|
+
}) as unknown as StateSource<unknown>,
|
|
33
|
+
),
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
|
|
19
37
|
describe('queryStore', () => {
|
|
20
38
|
let instance: SanityInstance
|
|
21
39
|
let liveEvents: Subject<LiveEvent>
|
|
@@ -56,6 +74,7 @@ describe('queryStore', () => {
|
|
|
56
74
|
})
|
|
57
75
|
|
|
58
76
|
afterEach(() => {
|
|
77
|
+
vi.mocked(getClientState).mockClear()
|
|
59
78
|
instance.dispose()
|
|
60
79
|
})
|
|
61
80
|
|
|
@@ -428,4 +447,49 @@ describe('queryStore', () => {
|
|
|
428
447
|
|
|
429
448
|
base.dispose()
|
|
430
449
|
})
|
|
450
|
+
|
|
451
|
+
it('uses source from params when passed in query options (listenForNewSubscribersAndFetch)', async () => {
|
|
452
|
+
const query = '*[_type == "movie"]'
|
|
453
|
+
const mediaLibrarySource = {mediaLibraryId: 'ml123'}
|
|
454
|
+
|
|
455
|
+
const state = getQueryState(instance, {query, source: mediaLibrarySource})
|
|
456
|
+
const unsubscribe = state.subscribe()
|
|
457
|
+
|
|
458
|
+
await firstValueFrom(state.observable.pipe(filter((i) => i !== undefined)))
|
|
459
|
+
|
|
460
|
+
// Verify getClientState was called with the source from params in listenForNewSubscribersAndFetch
|
|
461
|
+
// This call includes projectId, dataset, and source
|
|
462
|
+
expect(getClientState).toHaveBeenCalledWith(
|
|
463
|
+
instance,
|
|
464
|
+
expect.objectContaining({
|
|
465
|
+
source: expect.objectContaining({
|
|
466
|
+
mediaLibraryId: 'ml123',
|
|
467
|
+
}),
|
|
468
|
+
}),
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
unsubscribe()
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
it('uses source from store context key when not a dataset source (listenToLiveClientAndSetLastLiveEventIds)', async () => {
|
|
475
|
+
const query = '*[_type == "movie"]'
|
|
476
|
+
const canvasSource = {canvasId: 'canvas456'}
|
|
477
|
+
|
|
478
|
+
const state = getQueryState(instance, {query, source: canvasSource})
|
|
479
|
+
const unsubscribe = state.subscribe()
|
|
480
|
+
|
|
481
|
+
await firstValueFrom(state.observable.pipe(filter((i) => i !== undefined)))
|
|
482
|
+
|
|
483
|
+
// Verify getClientState was called with the canvas source for live events
|
|
484
|
+
// The source is extracted from the store key and passed when it's not a dataset source
|
|
485
|
+
// This call only has apiVersion and source (no projectId/dataset)
|
|
486
|
+
const calls = vi.mocked(getClientState).mock.calls
|
|
487
|
+
const liveClientCall = calls.find(
|
|
488
|
+
([_instance, options]) =>
|
|
489
|
+
isCanvasSource(options.source!) && options.source.canvasId === 'canvas456',
|
|
490
|
+
)
|
|
491
|
+
expect(liveClientCall).toBeDefined()
|
|
492
|
+
|
|
493
|
+
unsubscribe()
|
|
494
|
+
})
|
|
431
495
|
})
|
package/src/query/queryStore.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
mergeMap,
|
|
15
15
|
NEVER,
|
|
16
16
|
Observable,
|
|
17
|
+
of,
|
|
17
18
|
pairwise,
|
|
18
19
|
race,
|
|
19
20
|
share,
|
|
@@ -23,9 +24,18 @@ import {
|
|
|
23
24
|
} from 'rxjs'
|
|
24
25
|
|
|
25
26
|
import {getClientState} from '../client/clientStore'
|
|
26
|
-
import {type DatasetHandle,
|
|
27
|
+
import {type DatasetHandle, isDatasetSource} from '../config/sanityConfig'
|
|
28
|
+
/*
|
|
29
|
+
* Although this is an import dependency cycle, it is not a logical cycle:
|
|
30
|
+
* 1. queryStore uses getPerspectiveState when resolving release perspectives
|
|
31
|
+
* 2. getPerspectiveState uses releasesStore as a data source
|
|
32
|
+
* 3. releasesStore uses queryStore as a data source
|
|
33
|
+
* 4. however, queryStore does not use getPerspectiveState for the perspective used in releasesStore ("raw")
|
|
34
|
+
*/
|
|
35
|
+
// eslint-disable-next-line import/no-cycle
|
|
27
36
|
import {getPerspectiveState} from '../releases/getPerspectiveState'
|
|
28
|
-
import {
|
|
37
|
+
import {isReleasePerspective} from '../releases/utils/isReleasePerspective'
|
|
38
|
+
import {bindActionBySource, type BoundSourceKey} from '../store/createActionBinder'
|
|
29
39
|
import {type SanityInstance} from '../store/createSanityInstance'
|
|
30
40
|
import {
|
|
31
41
|
createStateSourceAction,
|
|
@@ -64,7 +74,6 @@ export interface QueryOptions<
|
|
|
64
74
|
DatasetHandle<TDataset, TProjectId> {
|
|
65
75
|
query: TQuery
|
|
66
76
|
params?: Record<string, unknown>
|
|
67
|
-
source?: DocumentSource
|
|
68
77
|
}
|
|
69
78
|
|
|
70
79
|
/**
|
|
@@ -107,7 +116,7 @@ function normalizeOptionsWithPerspective(
|
|
|
107
116
|
}
|
|
108
117
|
}
|
|
109
118
|
|
|
110
|
-
const queryStore = defineStore<QueryStoreState>({
|
|
119
|
+
const queryStore = defineStore<QueryStoreState, BoundSourceKey>({
|
|
111
120
|
name: 'QueryStore',
|
|
112
121
|
getInitialState: () => ({queries: {}}),
|
|
113
122
|
initialize(context) {
|
|
@@ -168,9 +177,13 @@ const listenForNewSubscribersAndFetch = ({state, instance}: StoreContext<QuerySt
|
|
|
168
177
|
...restOptions
|
|
169
178
|
} = parseQueryKey(group$.key)
|
|
170
179
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
180
|
+
// Short-circuit perspective resolution for non-release perspectives to avoid
|
|
181
|
+
// touching the releases store (and its initialization) unnecessarily.
|
|
182
|
+
const perspective$ = isReleasePerspective(perspectiveFromOptions)
|
|
183
|
+
? getPerspectiveState(instance, {
|
|
184
|
+
perspective: perspectiveFromOptions,
|
|
185
|
+
}).observable.pipe(filter(Boolean))
|
|
186
|
+
: of(perspectiveFromOptions ?? QUERY_STORE_DEFAULT_PERSPECTIVE)
|
|
174
187
|
|
|
175
188
|
const client$ = getClientState(instance, {
|
|
176
189
|
apiVersion: QUERY_STORE_API_VERSION,
|
|
@@ -179,8 +192,12 @@ const listenForNewSubscribersAndFetch = ({state, instance}: StoreContext<QuerySt
|
|
|
179
192
|
source,
|
|
180
193
|
}).observable
|
|
181
194
|
|
|
182
|
-
return combineLatest(
|
|
183
|
-
|
|
195
|
+
return combineLatest({
|
|
196
|
+
lastLiveEventId: lastLiveEventId$,
|
|
197
|
+
client: client$,
|
|
198
|
+
perspective: perspective$,
|
|
199
|
+
}).pipe(
|
|
200
|
+
switchMap(({lastLiveEventId, client, perspective}) =>
|
|
184
201
|
client.observable.fetch(query, params, {
|
|
185
202
|
...restOptions,
|
|
186
203
|
perspective,
|
|
@@ -208,9 +225,12 @@ const listenForNewSubscribersAndFetch = ({state, instance}: StoreContext<QuerySt
|
|
|
208
225
|
const listenToLiveClientAndSetLastLiveEventIds = ({
|
|
209
226
|
state,
|
|
210
227
|
instance,
|
|
211
|
-
|
|
228
|
+
key: {source},
|
|
229
|
+
}: StoreContext<QueryStoreState, BoundSourceKey>) => {
|
|
212
230
|
const liveMessages$ = getClientState(instance, {
|
|
213
231
|
apiVersion: QUERY_STORE_API_VERSION,
|
|
232
|
+
// temporary guard here until we're ready for everything to be queried via global api
|
|
233
|
+
...(source && !isDatasetSource(source) ? {source} : {}),
|
|
214
234
|
}).observable.pipe(
|
|
215
235
|
switchMap((client) =>
|
|
216
236
|
defer(() =>
|