@sanity/sdk 2.5.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 +429 -27
- package/dist/index.js +657 -266
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
- package/src/_exports/index.ts +18 -3
- 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/client/clientStore.test.ts +45 -43
- package/src/client/clientStore.ts +23 -9
- package/src/config/loggingConfig.ts +149 -0
- package/src/config/sanityConfig.ts +82 -22
- 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 +33 -11
- 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 +102 -13
- package/src/store/createSanityInstance.test.ts +85 -1
- package/src/store/createSanityInstance.ts +55 -4
- package/src/utils/logger-usage-example.md +141 -0
- package/src/utils/logger.test.ts +757 -0
- package/src/utils/logger.ts +537 -0
|
@@ -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,
|
|
@@ -58,11 +68,12 @@ export interface QueryOptions<
|
|
|
58
68
|
TQuery extends string = string,
|
|
59
69
|
TDataset extends string = string,
|
|
60
70
|
TProjectId extends string = string,
|
|
61
|
-
>
|
|
71
|
+
>
|
|
72
|
+
extends
|
|
73
|
+
Pick<ResponseQueryOptions, 'useCdn' | 'cache' | 'next' | 'cacheMode' | 'tag'>,
|
|
62
74
|
DatasetHandle<TDataset, TProjectId> {
|
|
63
75
|
query: TQuery
|
|
64
76
|
params?: Record<string, unknown>
|
|
65
|
-
source?: DocumentSource
|
|
66
77
|
}
|
|
67
78
|
|
|
68
79
|
/**
|
|
@@ -105,7 +116,7 @@ function normalizeOptionsWithPerspective(
|
|
|
105
116
|
}
|
|
106
117
|
}
|
|
107
118
|
|
|
108
|
-
const queryStore = defineStore<QueryStoreState>({
|
|
119
|
+
const queryStore = defineStore<QueryStoreState, BoundSourceKey>({
|
|
109
120
|
name: 'QueryStore',
|
|
110
121
|
getInitialState: () => ({queries: {}}),
|
|
111
122
|
initialize(context) {
|
|
@@ -166,9 +177,13 @@ const listenForNewSubscribersAndFetch = ({state, instance}: StoreContext<QuerySt
|
|
|
166
177
|
...restOptions
|
|
167
178
|
} = parseQueryKey(group$.key)
|
|
168
179
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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)
|
|
172
187
|
|
|
173
188
|
const client$ = getClientState(instance, {
|
|
174
189
|
apiVersion: QUERY_STORE_API_VERSION,
|
|
@@ -177,8 +192,12 @@ const listenForNewSubscribersAndFetch = ({state, instance}: StoreContext<QuerySt
|
|
|
177
192
|
source,
|
|
178
193
|
}).observable
|
|
179
194
|
|
|
180
|
-
return combineLatest(
|
|
181
|
-
|
|
195
|
+
return combineLatest({
|
|
196
|
+
lastLiveEventId: lastLiveEventId$,
|
|
197
|
+
client: client$,
|
|
198
|
+
perspective: perspective$,
|
|
199
|
+
}).pipe(
|
|
200
|
+
switchMap(({lastLiveEventId, client, perspective}) =>
|
|
182
201
|
client.observable.fetch(query, params, {
|
|
183
202
|
...restOptions,
|
|
184
203
|
perspective,
|
|
@@ -206,9 +225,12 @@ const listenForNewSubscribersAndFetch = ({state, instance}: StoreContext<QuerySt
|
|
|
206
225
|
const listenToLiveClientAndSetLastLiveEventIds = ({
|
|
207
226
|
state,
|
|
208
227
|
instance,
|
|
209
|
-
|
|
228
|
+
key: {source},
|
|
229
|
+
}: StoreContext<QueryStoreState, BoundSourceKey>) => {
|
|
210
230
|
const liveMessages$ = getClientState(instance, {
|
|
211
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} : {}),
|
|
212
234
|
}).observable.pipe(
|
|
213
235
|
switchMap((client) =>
|
|
214
236
|
defer(() =>
|
|
@@ -2,14 +2,13 @@ import {filter, firstValueFrom, of, Subject, take} from 'rxjs'
|
|
|
2
2
|
import {describe, expect, it, vi} from 'vitest'
|
|
3
3
|
|
|
4
4
|
import {type PerspectiveHandle, type ReleasePerspective} from '../config/sanityConfig'
|
|
5
|
+
import {getQueryState} from '../query/queryStore'
|
|
5
6
|
import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
|
|
6
|
-
import {
|
|
7
|
+
import {type StateSource} from '../store/createStateSourceAction'
|
|
7
8
|
import {getPerspectiveState} from './getPerspectiveState'
|
|
8
9
|
import {type ReleaseDocument} from './releasesStore'
|
|
9
10
|
|
|
10
|
-
vi.mock('../
|
|
11
|
-
listenQuery: vi.fn(),
|
|
12
|
-
}))
|
|
11
|
+
vi.mock('../query/queryStore')
|
|
13
12
|
|
|
14
13
|
vi.mock('../client/clientStore', () => ({
|
|
15
14
|
getClientState: vi.fn(() => ({
|
|
@@ -21,7 +20,7 @@ vi.mock('../client/clientStore', () => ({
|
|
|
21
20
|
|
|
22
21
|
describe('getPerspectiveState', () => {
|
|
23
22
|
let instance: SanityInstance
|
|
24
|
-
let mockReleasesQuerySubject: Subject<ReleaseDocument[]>
|
|
23
|
+
let mockReleasesQuerySubject: Subject<ReleaseDocument[] | undefined>
|
|
25
24
|
|
|
26
25
|
const release1 = {
|
|
27
26
|
_id: 'release-1',
|
|
@@ -44,8 +43,12 @@ describe('getPerspectiveState', () => {
|
|
|
44
43
|
beforeEach(() => {
|
|
45
44
|
instance = createSanityInstance({projectId: 'test', dataset: 'test'})
|
|
46
45
|
|
|
47
|
-
mockReleasesQuerySubject = new Subject<ReleaseDocument[]>()
|
|
48
|
-
vi.mocked(
|
|
46
|
+
mockReleasesQuerySubject = new Subject<ReleaseDocument[] | undefined>()
|
|
47
|
+
vi.mocked(getQueryState).mockReturnValue({
|
|
48
|
+
subscribe: () => () => {},
|
|
49
|
+
getCurrent: () => undefined,
|
|
50
|
+
observable: mockReleasesQuerySubject.asObservable(),
|
|
51
|
+
} as StateSource<ReleaseDocument[] | undefined>)
|
|
49
52
|
})
|
|
50
53
|
|
|
51
54
|
afterEach(() => {
|
|
@@ -95,7 +98,7 @@ describe('getPerspectiveState', () => {
|
|
|
95
98
|
take(1),
|
|
96
99
|
),
|
|
97
100
|
)
|
|
98
|
-
expect(perspective).toEqual(['
|
|
101
|
+
expect(perspective).toEqual(['release1', 'drafts'])
|
|
99
102
|
})
|
|
100
103
|
|
|
101
104
|
it('should calculate perspective including multiple releases up to the specified releaseName', async () => {
|
|
@@ -108,13 +111,13 @@ describe('getPerspectiveState', () => {
|
|
|
108
111
|
take(1),
|
|
109
112
|
),
|
|
110
113
|
)
|
|
111
|
-
expect(perspective).toEqual(['
|
|
114
|
+
expect(perspective).toEqual(['release2', 'release1', 'drafts'])
|
|
112
115
|
})
|
|
113
116
|
|
|
114
117
|
it('should filter excluded perspectives', async () => {
|
|
115
118
|
const perspectiveConfig: ReleasePerspective = {
|
|
116
119
|
releaseName: 'release2',
|
|
117
|
-
excludedPerspectives: ['
|
|
120
|
+
excludedPerspectives: ['release1', 'drafts'],
|
|
118
121
|
}
|
|
119
122
|
const options: PerspectiveHandle = {perspective: perspectiveConfig}
|
|
120
123
|
const state = getPerspectiveState(instance, options)
|
|
@@ -159,7 +162,7 @@ describe('getPerspectiveState', () => {
|
|
|
159
162
|
const state2 = getPerspectiveState(instance, options2)
|
|
160
163
|
const perspective2 = state2.getCurrent()
|
|
161
164
|
|
|
162
|
-
expect(perspective2).toEqual(['
|
|
165
|
+
expect(perspective2).toEqual(['release1', 'drafts'])
|
|
163
166
|
})
|
|
164
167
|
|
|
165
168
|
it('should handle changes in activeReleases (cache test)', async () => {
|
|
@@ -173,7 +176,7 @@ describe('getPerspectiveState', () => {
|
|
|
173
176
|
take(1),
|
|
174
177
|
),
|
|
175
178
|
)
|
|
176
|
-
expect(perspective1).toEqual(['
|
|
179
|
+
expect(perspective1).toEqual(['release1', 'drafts'])
|
|
177
180
|
|
|
178
181
|
const updatedActiveReleases = [release1]
|
|
179
182
|
mockReleasesQuerySubject.next(updatedActiveReleases)
|
|
@@ -184,10 +187,10 @@ describe('getPerspectiveState', () => {
|
|
|
184
187
|
take(1),
|
|
185
188
|
),
|
|
186
189
|
)
|
|
187
|
-
expect(perspectiveAfterUpdate).toEqual(['
|
|
190
|
+
expect(perspectiveAfterUpdate).toEqual(['release1', 'drafts'])
|
|
188
191
|
|
|
189
192
|
const state2 = getPerspectiveState(instance, options)
|
|
190
193
|
const perspectiveNewCall = state2.getCurrent()
|
|
191
|
-
expect(perspectiveNewCall).toEqual(['
|
|
194
|
+
expect(perspectiveNewCall).toEqual(['release1', 'drafts'])
|
|
192
195
|
})
|
|
193
196
|
})
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import {createSelector} from 'reselect'
|
|
2
2
|
|
|
3
|
-
import {type PerspectiveHandle
|
|
4
|
-
import {bindActionByDataset} from '../store/createActionBinder'
|
|
3
|
+
import {type PerspectiveHandle} from '../config/sanityConfig'
|
|
4
|
+
import {bindActionByDataset, type BoundStoreAction} from '../store/createActionBinder'
|
|
5
5
|
import {createStateSourceAction, type SelectorContext} from '../store/createStateSourceAction'
|
|
6
|
+
/*
|
|
7
|
+
* Although this is an import dependency cycle, it is not a logical cycle:
|
|
8
|
+
* 1. getPerspectiveState uses releasesStore as a data source
|
|
9
|
+
* 2. releasesStore uses queryStore as a data source
|
|
10
|
+
* 3. queryStore calls getPerspectiveState for computing release perspectives
|
|
11
|
+
* 4. however, queryStore does not use getPerspectiveState for the perspective used in releasesStore ("raw")
|
|
12
|
+
*/
|
|
13
|
+
// eslint-disable-next-line import/no-cycle
|
|
6
14
|
import {releasesStore, type ReleasesStoreState} from './releasesStore'
|
|
15
|
+
import {isReleasePerspective} from './utils/isReleasePerspective'
|
|
7
16
|
import {sortReleases} from './utils/sortReleases'
|
|
8
17
|
|
|
9
|
-
function isReleasePerspective(
|
|
10
|
-
perspective: PerspectiveHandle['perspective'],
|
|
11
|
-
): perspective is ReleasePerspective {
|
|
12
|
-
return typeof perspective === 'object' && perspective !== null && 'releaseName' in perspective
|
|
13
|
-
}
|
|
14
|
-
|
|
15
18
|
const DEFAULT_PERSPECTIVE = 'drafts'
|
|
16
19
|
|
|
17
20
|
// Cache for options
|
|
@@ -50,6 +53,44 @@ const memoizedOptionsSelector = createSelector(
|
|
|
50
53
|
},
|
|
51
54
|
)
|
|
52
55
|
|
|
56
|
+
// Lazily bind the action itself to avoid circular import initialization issues with `releasesStore`
|
|
57
|
+
const _getPerspectiveStateSelector = createStateSourceAction({
|
|
58
|
+
selector: createSelector(
|
|
59
|
+
[selectInstancePerspective, selectActiveReleases, memoizedOptionsSelector],
|
|
60
|
+
(instancePerspective, activeReleases, memoizedOptions) => {
|
|
61
|
+
const perspective = memoizedOptions?.perspective ?? instancePerspective ?? DEFAULT_PERSPECTIVE
|
|
62
|
+
|
|
63
|
+
if (!isReleasePerspective(perspective)) return perspective
|
|
64
|
+
|
|
65
|
+
// if there are no active releases we can't compute the release perspective
|
|
66
|
+
if (!activeReleases || activeReleases.length === 0) return undefined
|
|
67
|
+
|
|
68
|
+
const releaseNames = sortReleases(activeReleases).map((release) => release.name)
|
|
69
|
+
const index = releaseNames.findIndex((name) => name === perspective.releaseName)
|
|
70
|
+
|
|
71
|
+
if (index < 0) {
|
|
72
|
+
throw new Error(`Release "${perspective.releaseName}" not found in active releases`)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const filteredReleases = releaseNames.slice(0, index + 1) // Include the release itself
|
|
76
|
+
|
|
77
|
+
return ['drafts', ...filteredReleases]
|
|
78
|
+
.filter((name) => !perspective.excludedPerspectives?.includes(name))
|
|
79
|
+
.reverse()
|
|
80
|
+
},
|
|
81
|
+
),
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
type OmitFirst<T extends unknown[]> = T extends [unknown, ...infer R] ? R : never
|
|
85
|
+
type SelectorParams = OmitFirst<Parameters<typeof _getPerspectiveStateSelector>>
|
|
86
|
+
type BoundGetPerspectiveState = BoundStoreAction<
|
|
87
|
+
ReleasesStoreState,
|
|
88
|
+
SelectorParams,
|
|
89
|
+
ReturnType<typeof _getPerspectiveStateSelector>
|
|
90
|
+
>
|
|
91
|
+
|
|
92
|
+
let _boundGetPerspectiveState: BoundGetPerspectiveState | undefined
|
|
93
|
+
|
|
53
94
|
/**
|
|
54
95
|
* Provides a subscribable state source for a "perspective" for the Sanity client,
|
|
55
96
|
* which is used to fetch documents as though certain Content Releases are active.
|
|
@@ -62,33 +103,12 @@ const memoizedOptionsSelector = createSelector(
|
|
|
62
103
|
*
|
|
63
104
|
* @public
|
|
64
105
|
*/
|
|
65
|
-
export const getPerspectiveState =
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (!isReleasePerspective(perspective)) return perspective
|
|
75
|
-
|
|
76
|
-
// if there are no active releases we can't compute the release perspective
|
|
77
|
-
if (!activeReleases || activeReleases.length === 0) return undefined
|
|
78
|
-
|
|
79
|
-
const releaseNames = sortReleases(activeReleases).map((release) => release.name)
|
|
80
|
-
const index = releaseNames.findIndex((name) => name === perspective.releaseName)
|
|
81
|
-
|
|
82
|
-
if (index < 0) {
|
|
83
|
-
throw new Error(`Release "${perspective.releaseName}" not found in active releases`)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const filteredReleases = releaseNames.slice(0, index + 1) // Include the release itself
|
|
87
|
-
|
|
88
|
-
return ['drafts', ...filteredReleases].filter(
|
|
89
|
-
(name) => !perspective.excludedPerspectives?.includes(name),
|
|
90
|
-
)
|
|
91
|
-
},
|
|
92
|
-
),
|
|
93
|
-
}),
|
|
94
|
-
)
|
|
106
|
+
export const getPerspectiveState: BoundGetPerspectiveState = (...args) => {
|
|
107
|
+
if (!_boundGetPerspectiveState) {
|
|
108
|
+
_boundGetPerspectiveState = bindActionByDataset(
|
|
109
|
+
releasesStore,
|
|
110
|
+
_getPerspectiveStateSelector,
|
|
111
|
+
) as BoundGetPerspectiveState
|
|
112
|
+
}
|
|
113
|
+
return _boundGetPerspectiveState(...args)
|
|
114
|
+
}
|