@sanity/sdk 3.0.0-rc.0 → 3.0.0-rc.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/_chunks-dts/createGroqSearchFilter.d.ts +1147 -0
- package/dist/_chunks-dts/createGroqSearchFilter.d.ts.map +1 -0
- package/dist/_chunks-es/_internal.js +134 -0
- package/dist/_chunks-es/_internal.js.map +1 -0
- package/dist/_chunks-es/createGroqSearchFilter.js +1864 -0
- package/dist/_chunks-es/createGroqSearchFilter.js.map +1 -0
- package/dist/_chunks-es/telemetryManager.js +93 -0
- package/dist/_chunks-es/telemetryManager.js.map +1 -0
- package/dist/_exports/_internal.d.ts +95 -0
- package/dist/_exports/_internal.d.ts.map +1 -0
- package/dist/_exports/_internal.js +24 -0
- package/dist/_exports/_internal.js.map +1 -0
- package/dist/index.d.ts +1079 -1076
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1967 -2302
- package/dist/index.js.map +1 -1
- package/package.json +48 -47
- package/src/_exports/_internal.ts +26 -0
- package/src/_exports/index.ts +120 -11
- package/src/access/checkPermissions.test.ts +55 -0
- package/src/access/checkPermissions.ts +54 -0
- package/src/agent/agentActions.test.ts +16 -60
- package/src/agent/agentActions.ts +25 -38
- package/src/applications/applications.test-d.ts +83 -0
- package/src/applications/applications.test.ts +130 -0
- package/src/applications/applications.ts +364 -0
- package/src/applications/userApplications.test.ts +126 -0
- package/src/applications/userApplications.ts +261 -0
- package/src/auth/authLogger.ts +30 -0
- package/src/auth/authMode.test.ts +25 -0
- package/src/auth/authMode.ts +6 -3
- package/src/auth/authStore.test.ts +194 -13
- package/src/auth/authStore.ts +59 -26
- package/src/auth/getOrganizationVerificationState.test.ts +18 -17
- package/src/auth/getOrganizationVerificationState.ts +4 -4
- package/src/auth/handleAuthCallback.test.ts +35 -1
- package/src/auth/handleAuthCallback.ts +25 -6
- package/src/auth/logout.test.ts +75 -2
- package/src/auth/logout.ts +23 -4
- package/src/auth/refreshStampedToken.test.ts +219 -469
- package/src/auth/refreshStampedToken.ts +79 -195
- package/src/auth/studioAuth.ts +1 -1
- package/src/auth/studioModeAuth.ts +2 -1
- package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +146 -13
- package/src/auth/subscribeToStateAndFetchCurrentUser.ts +37 -6
- package/src/auth/subscribeToStorageEventsAndSetToken.test.ts +2 -0
- package/src/client/clientStore.test.ts +62 -37
- package/src/client/clientStore.ts +45 -75
- package/src/client/liveEvents.test.ts +151 -0
- package/src/client/liveEvents.ts +107 -0
- package/src/comlink/controller/actions/destroyController.test.ts +4 -1
- package/src/comlink/controller/actions/getOrCreateChannel.test.ts +4 -1
- package/src/comlink/controller/actions/getOrCreateChannel.ts +2 -2
- package/src/comlink/controller/actions/getOrCreateController.test.ts +4 -1
- package/src/comlink/controller/actions/releaseChannel.test.ts +1 -1
- package/src/comlink/controller/comlinkControllerStore.test.ts +4 -1
- package/src/comlink/node/actions/getOrCreateNode.test.ts +9 -3
- package/src/comlink/node/actions/getOrCreateNode.ts +2 -2
- package/src/comlink/node/actions/releaseNode.test.ts +7 -4
- package/src/comlink/node/comlinkNodeStore.test.ts +2 -2
- package/src/comlink/node/getNodeState.test.ts +1 -1
- package/src/comlink/node/getNodeState.ts +2 -1
- package/src/config/__tests__/handles.test.ts +18 -12
- package/src/config/handles.ts +25 -7
- package/src/config/sanityConfig.ts +109 -79
- package/src/datasets/datasets.test.ts +2 -2
- package/src/datasets/datasets.ts +10 -4
- package/src/document/actions.test.ts +115 -33
- package/src/document/actions.ts +212 -15
- package/src/document/applyDocumentActions.test.ts +38 -12
- package/src/document/applyDocumentActions.ts +30 -9
- package/src/document/documentConstants.ts +7 -0
- package/src/document/documentStore.concurrency.test.ts +1306 -0
- package/src/document/documentStore.test.ts +673 -278
- package/src/document/documentStore.ts +162 -71
- package/src/document/events.test.ts +57 -2
- package/src/document/events.ts +82 -26
- package/src/document/listen.test.ts +56 -0
- package/src/document/listen.ts +74 -21
- package/src/document/listenerEventOperators.test.ts +311 -0
- package/src/document/listenerEventOperators.ts +217 -0
- package/src/document/patchOperations.test.ts +49 -2
- package/src/document/patchOperations.ts +50 -0
- package/src/document/permissions.test.ts +105 -60
- package/src/document/permissions.ts +44 -19
- package/src/document/processActions/create.ts +138 -0
- package/src/document/processActions/delete.ts +100 -0
- package/src/document/processActions/discard.ts +63 -0
- package/src/document/processActions/edit.ts +154 -0
- package/src/document/processActions/processActions.ts +218 -0
- package/src/document/processActions/publish.ts +120 -0
- package/src/document/processActions/releaseArchive.ts +77 -0
- package/src/document/processActions/releaseCreate.ts +59 -0
- package/src/document/processActions/releaseDelete.ts +65 -0
- package/src/document/processActions/releaseEdit.ts +37 -0
- package/src/document/processActions/releasePublish.ts +45 -0
- package/src/document/processActions/releaseSchedule.ts +87 -0
- package/src/document/processActions/releaseUtil.ts +31 -0
- package/src/document/processActions/shared.ts +308 -0
- package/src/document/processActions/unpublish.ts +85 -0
- package/src/document/processActions.test.ts +1129 -121
- package/src/document/reducers.test.ts +639 -64
- package/src/document/reducers.ts +187 -48
- package/src/document/resourceRules.test.ts +178 -0
- package/src/document/resourceRules.ts +117 -0
- package/src/document/sharedListener.test.ts +16 -14
- package/src/document/sharedListener.ts +24 -8
- package/src/document/util.ts +14 -0
- package/src/favorites/favorites.test.ts +5 -6
- package/src/installations/installations.test-d.ts +68 -0
- package/src/installations/installations.test.ts +62 -0
- package/src/installations/installations.ts +202 -0
- package/src/organization/organization.test-d.ts +36 -0
- package/src/organization/organization.test.ts +138 -0
- package/src/organization/organization.ts +138 -0
- package/src/organizations/organizations.test-d.ts +56 -0
- package/src/organizations/organizations.test.ts +150 -0
- package/src/organizations/organizations.ts +107 -0
- package/src/presence/bifurTransport.test.ts +46 -6
- package/src/presence/bifurTransport.ts +19 -2
- package/src/presence/presenceStore.test.ts +94 -23
- package/src/presence/presenceStore.ts +85 -29
- package/src/preview/getPreviewState.test.ts +137 -0
- package/src/preview/getPreviewState.ts +69 -0
- package/src/preview/previewProjectionUtils.test.ts +9 -22
- package/src/preview/previewProjectionUtils.ts +7 -3
- package/src/preview/resolvePreview.test.ts +64 -0
- package/src/preview/resolvePreview.ts +43 -0
- package/src/preview/types.ts +10 -0
- package/src/project/project.test-d.ts +37 -0
- package/src/project/project.test.ts +157 -12
- package/src/project/project.ts +138 -25
- package/src/projection/getProjectionState.test.ts +18 -20
- package/src/projection/getProjectionState.ts +23 -3
- package/src/projection/projectionQuery.ts +5 -5
- package/src/projection/projectionStore.test.ts +1 -1
- package/src/projection/resolveProjection.test.ts +1 -2
- package/src/projection/resolveProjection.ts +16 -0
- package/src/projection/subscribeToStateAndFetchBatches.test.ts +1 -1
- package/src/projection/subscribeToStateAndFetchBatches.ts +32 -23
- package/src/projection/types.ts +8 -0
- package/src/projects/projects.test-d.ts +32 -0
- package/src/projects/projects.test.ts +108 -42
- package/src/projects/projects.ts +63 -27
- package/src/query/queryStore.test.ts +259 -123
- package/src/query/queryStore.ts +78 -63
- package/src/query/reducers.test.ts +0 -42
- package/src/query/reducers.ts +2 -18
- package/src/releases/getPerspectiveState.test.ts +17 -23
- package/src/releases/getPerspectiveState.ts +8 -20
- package/src/releases/observeReleases.test.ts +127 -0
- package/src/releases/observeReleases.ts +102 -0
- package/src/releases/releasesStore.test.ts +100 -53
- package/src/releases/releasesStore.ts +69 -44
- package/src/releases/utils/sortReleases.test.ts +38 -2
- package/src/releases/utils/sortReleases.ts +15 -10
- package/src/store/createActionBinder.test.ts +87 -90
- package/src/store/createActionBinder.ts +79 -25
- package/src/store/createSanityInstance.test.ts +83 -13
- package/src/store/createSanityInstance.ts +77 -2
- package/src/store/createStateSourceAction.test.ts +64 -2
- package/src/store/createStateSourceAction.ts +39 -44
- package/src/store/createStoreInstance.test.ts +4 -2
- package/src/store/fetcherStore.test-d.ts +121 -0
- package/src/store/fetcherStore.test.ts +299 -0
- package/src/store/fetcherStore.ts +801 -0
- package/src/telemetry/environment.test.ts +119 -0
- package/src/telemetry/environment.ts +92 -0
- package/src/telemetry/events.ts +42 -0
- package/src/telemetry/initTelemetry.test.ts +432 -0
- package/src/telemetry/initTelemetry.ts +218 -0
- package/src/telemetry/telemetryManager.test.ts +327 -0
- package/src/telemetry/telemetryManager.ts +199 -0
- package/src/users/reducers.test.ts +6 -1
- package/src/users/reducers.ts +5 -6
- package/src/users/types.ts +4 -4
- package/src/users/usersStore.test.ts +16 -12
- package/src/users/usersStore.ts +5 -1
- package/src/utils/buildQuery.test.ts +14 -0
- package/src/utils/buildQuery.ts +17 -0
- package/src/utils/createFetcherStore.test.ts +7 -5
- package/src/utils/createFetcherStore.ts +8 -5
- package/src/utils/getStagingApiHost.test.ts +21 -0
- package/src/utils/getStagingApiHost.ts +14 -0
- package/src/utils/ids.test.ts +1 -29
- package/src/utils/ids.ts +0 -10
- package/src/utils/includeShape.ts +36 -0
- package/src/utils/isImportError.test.ts +72 -0
- package/src/utils/isImportError.ts +34 -0
- package/src/utils/logger.test.ts +12 -0
- package/src/utils/logger.ts +8 -3
- package/src/utils/object.test.ts +95 -0
- package/src/utils/object.ts +142 -0
- package/src/utils/setCleanupTimeout.ts +37 -0
- package/src/document/processActions.ts +0 -707
- package/src/utils/defineIntent.test.ts +0 -477
- package/src/utils/defineIntent.ts +0 -244
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import {type ReleaseDocument, type SyncTag} from '@sanity/client'
|
|
2
|
+
import {
|
|
3
|
+
BehaviorSubject,
|
|
4
|
+
combineLatest,
|
|
5
|
+
distinctUntilChanged,
|
|
6
|
+
filter,
|
|
7
|
+
map,
|
|
8
|
+
type Observable,
|
|
9
|
+
startWith,
|
|
10
|
+
switchMap,
|
|
11
|
+
tap,
|
|
12
|
+
} from 'rxjs'
|
|
13
|
+
|
|
14
|
+
import {getClientState} from '../client/clientStore'
|
|
15
|
+
import {observeLiveEvents} from '../client/liveEvents'
|
|
16
|
+
import {type DocumentResource} from '../config/sanityConfig'
|
|
17
|
+
import {type SanityInstance} from '../store/createSanityInstance'
|
|
18
|
+
|
|
19
|
+
const RELEASES_QUERY = 'releases::all()'
|
|
20
|
+
const RELEASES_API_VERSION = 'v2025-05-06'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Options for {@link observeReleases}.
|
|
24
|
+
* @internal
|
|
25
|
+
*/
|
|
26
|
+
export interface ObserveReleasesOptions {
|
|
27
|
+
/** Resource to scope the underlying client and live connection to. */
|
|
28
|
+
resource?: DocumentResource
|
|
29
|
+
/**
|
|
30
|
+
* Called when the live events connection fails due to a CORS
|
|
31
|
+
* misconfiguration. See `observeLiveEvents`.
|
|
32
|
+
*/
|
|
33
|
+
onCorsError: (error: unknown) => void
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Emits every release document (including archived and published), refetching
|
|
38
|
+
* whenever a Live Content API event matches the query's sync tags.
|
|
39
|
+
*
|
|
40
|
+
* This fetches directly through the client store — deliberately not via the
|
|
41
|
+
* query store — because release data is itself an input to query perspective
|
|
42
|
+
* resolution (`getPerspectiveState`); fetching through the query store would
|
|
43
|
+
* make the two stores mutually dependent.
|
|
44
|
+
*
|
|
45
|
+
* @internal
|
|
46
|
+
*/
|
|
47
|
+
export function observeReleases(
|
|
48
|
+
instance: SanityInstance,
|
|
49
|
+
{resource, onCorsError}: ObserveReleasesOptions,
|
|
50
|
+
): Observable<ReleaseDocument[] | undefined> {
|
|
51
|
+
const client$ = getClientState(instance, {
|
|
52
|
+
apiVersion: RELEASES_API_VERSION,
|
|
53
|
+
resource,
|
|
54
|
+
}).observable
|
|
55
|
+
|
|
56
|
+
return client$.pipe(
|
|
57
|
+
switchMap((client) => {
|
|
58
|
+
const syncTags$ = new BehaviorSubject<SyncTag[] | undefined>(undefined)
|
|
59
|
+
|
|
60
|
+
// Pair the latest live message with the latest known sync tags so that
|
|
61
|
+
// a message arriving while a fetch is still in flight is re-evaluated
|
|
62
|
+
// (and refetched if it matches) once that fetch's tags land — instead
|
|
63
|
+
// of being dropped and leaving the store stale until the next event.
|
|
64
|
+
const refetchEventId$ = combineLatest([
|
|
65
|
+
observeLiveEvents(instance, {resource, onCorsError}).pipe(startWith(undefined)),
|
|
66
|
+
syncTags$,
|
|
67
|
+
]).pipe(
|
|
68
|
+
filter(
|
|
69
|
+
([message, syncTags]) =>
|
|
70
|
+
message === undefined || message.tags.some((tag) => syncTags?.includes(tag)),
|
|
71
|
+
),
|
|
72
|
+
map(([message]) => message?.id),
|
|
73
|
+
distinctUntilChanged(),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return refetchEventId$.pipe(
|
|
77
|
+
switchMap((lastLiveEventId) =>
|
|
78
|
+
client.observable.fetch<ReleaseDocument[]>(
|
|
79
|
+
RELEASES_QUERY,
|
|
80
|
+
{},
|
|
81
|
+
{
|
|
82
|
+
perspective: 'raw',
|
|
83
|
+
filterResponse: false,
|
|
84
|
+
returnQuery: false,
|
|
85
|
+
lastLiveEventId,
|
|
86
|
+
tag: 'releases',
|
|
87
|
+
},
|
|
88
|
+
),
|
|
89
|
+
),
|
|
90
|
+
tap((response) => {
|
|
91
|
+
syncTags$.next(response.syncTags)
|
|
92
|
+
}),
|
|
93
|
+
map((response) => response.result),
|
|
94
|
+
)
|
|
95
|
+
}),
|
|
96
|
+
// Emit synchronously on subscribe (mirroring StateSource semantics, which
|
|
97
|
+
// the releases store previously consumed) so the store can immediately
|
|
98
|
+
// record an empty list: consumers distinguish "no releases yet" from
|
|
99
|
+
// "never loaded", and the React hooks don't suspend on first load.
|
|
100
|
+
startWith(undefined),
|
|
101
|
+
)
|
|
102
|
+
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
+
import {type ReleaseDocument} from '@sanity/client'
|
|
1
2
|
import {NEVER, Observable, type Observer, of, Subject} from 'rxjs'
|
|
2
3
|
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
|
|
3
4
|
|
|
4
|
-
import {getQueryState, resolveQuery} from '../query/queryStore'
|
|
5
5
|
import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
|
|
6
|
-
import {
|
|
7
|
-
import {getActiveReleasesState,
|
|
6
|
+
import {observeReleases} from './observeReleases'
|
|
7
|
+
import {getActiveReleasesState, getAllReleasesState} from './releasesStore'
|
|
8
8
|
|
|
9
9
|
// Mock dependencies
|
|
10
|
-
vi.mock('
|
|
10
|
+
vi.mock('./observeReleases')
|
|
11
11
|
|
|
12
12
|
describe('releasesStore', () => {
|
|
13
13
|
let instance: SanityInstance
|
|
@@ -15,32 +15,24 @@ describe('releasesStore', () => {
|
|
|
15
15
|
beforeEach(() => {
|
|
16
16
|
vi.clearAllMocks()
|
|
17
17
|
|
|
18
|
-
instance = createSanityInstance()
|
|
18
|
+
instance = createSanityInstance({projectId: 'test', dataset: 'test'})
|
|
19
19
|
|
|
20
|
-
vi.mocked(
|
|
21
|
-
subscribe: () => () => {},
|
|
22
|
-
getCurrent: () => undefined,
|
|
23
|
-
observable: NEVER as Observable<ReleaseDocument[] | undefined>,
|
|
24
|
-
} as StateSource<ReleaseDocument[] | undefined>)
|
|
25
|
-
|
|
26
|
-
vi.mocked(resolveQuery).mockResolvedValue(undefined)
|
|
20
|
+
vi.mocked(observeReleases).mockReturnValue(NEVER as Observable<ReleaseDocument[] | undefined>)
|
|
27
21
|
})
|
|
28
22
|
|
|
29
23
|
afterEach(() => {
|
|
30
24
|
instance.dispose()
|
|
31
25
|
})
|
|
32
26
|
|
|
33
|
-
it('supports calls
|
|
34
|
-
const state = getActiveReleasesState(instance
|
|
27
|
+
it('supports calls without options', () => {
|
|
28
|
+
const state = getActiveReleasesState(instance)
|
|
35
29
|
|
|
36
30
|
expect(state.getCurrent()).toBeUndefined()
|
|
37
|
-
expect(
|
|
31
|
+
expect(observeReleases).toHaveBeenCalledWith(
|
|
38
32
|
instance,
|
|
39
33
|
expect.objectContaining({
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
resource: {projectId: 'test', dataset: 'test'},
|
|
43
|
-
tag: 'releases',
|
|
34
|
+
resource: {dataset: 'test', projectId: 'test'},
|
|
35
|
+
onCorsError: expect.any(Function),
|
|
44
36
|
}),
|
|
45
37
|
)
|
|
46
38
|
})
|
|
@@ -51,11 +43,7 @@ describe('releasesStore', () => {
|
|
|
51
43
|
.fn<(observer: Observer<ReleaseDocument[] | undefined>) => () => void>()
|
|
52
44
|
.mockReturnValue(teardown)
|
|
53
45
|
|
|
54
|
-
vi.mocked(
|
|
55
|
-
subscribe: () => () => {},
|
|
56
|
-
getCurrent: () => undefined,
|
|
57
|
-
observable: new Observable(subscriber),
|
|
58
|
-
} as StateSource<ReleaseDocument[] | undefined>)
|
|
46
|
+
vi.mocked(observeReleases).mockReturnValue(new Observable(subscriber))
|
|
59
47
|
|
|
60
48
|
// note that the order of the releases is important here -- they get sorted
|
|
61
49
|
const mockReleases: ReleaseDocument[] = [
|
|
@@ -86,11 +74,7 @@ describe('releasesStore', () => {
|
|
|
86
74
|
|
|
87
75
|
it('should update active releases state when the query emits new data', async () => {
|
|
88
76
|
const releasesSubject = new Subject<ReleaseDocument[]>()
|
|
89
|
-
vi.mocked(
|
|
90
|
-
subscribe: () => () => {},
|
|
91
|
-
getCurrent: () => undefined,
|
|
92
|
-
observable: releasesSubject.asObservable(),
|
|
93
|
-
} as StateSource<ReleaseDocument[] | undefined>)
|
|
77
|
+
vi.mocked(observeReleases).mockReturnValue(releasesSubject.asObservable())
|
|
94
78
|
|
|
95
79
|
const state = getActiveReleasesState(instance, {resource: {projectId: 'test', dataset: 'test'}})
|
|
96
80
|
|
|
@@ -132,12 +116,8 @@ describe('releasesStore', () => {
|
|
|
132
116
|
})
|
|
133
117
|
|
|
134
118
|
it('should handle empty array from the query', async () => {
|
|
135
|
-
// Configure
|
|
136
|
-
vi.mocked(
|
|
137
|
-
subscribe: () => () => {},
|
|
138
|
-
getCurrent: () => [],
|
|
139
|
-
observable: of([]),
|
|
140
|
-
} as StateSource<ReleaseDocument[] | undefined>)
|
|
119
|
+
// Configure the releases source to return an empty array
|
|
120
|
+
vi.mocked(observeReleases).mockReturnValue(of([]))
|
|
141
121
|
|
|
142
122
|
const state = getActiveReleasesState(instance, {resource: {projectId: 'test', dataset: 'test'}})
|
|
143
123
|
|
|
@@ -148,39 +128,106 @@ describe('releasesStore', () => {
|
|
|
148
128
|
|
|
149
129
|
it('should handle null/undefined from the query by defaulting to empty array', async () => {
|
|
150
130
|
// Test null case
|
|
151
|
-
vi.mocked(
|
|
152
|
-
subscribe: () => () => {},
|
|
153
|
-
getCurrent: () => null as unknown as ReleaseDocument[] | undefined,
|
|
154
|
-
observable: of(null as unknown as ReleaseDocument[] | undefined),
|
|
155
|
-
} as StateSource<ReleaseDocument[] | undefined>)
|
|
131
|
+
vi.mocked(observeReleases).mockReturnValue(of(null as unknown as ReleaseDocument[] | undefined))
|
|
156
132
|
const state = getActiveReleasesState(instance, {resource: {projectId: 'test', dataset: 'test'}})
|
|
157
133
|
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
158
134
|
expect(state.getCurrent()).toEqual([])
|
|
159
135
|
|
|
160
136
|
// Test undefined case
|
|
161
|
-
vi.mocked(
|
|
162
|
-
subscribe: () => () => {},
|
|
163
|
-
getCurrent: () => undefined,
|
|
164
|
-
observable: of(undefined),
|
|
165
|
-
} as StateSource<ReleaseDocument[] | undefined>)
|
|
137
|
+
vi.mocked(observeReleases).mockReturnValue(of(undefined))
|
|
166
138
|
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
167
139
|
expect(state.getCurrent()).toEqual([])
|
|
168
140
|
})
|
|
169
141
|
|
|
170
|
-
it('
|
|
142
|
+
it('exposes archived/published releases through getAllReleasesState but filters them out of getActiveReleasesState', async () => {
|
|
143
|
+
const subject = new Subject<ReleaseDocument[]>()
|
|
144
|
+
vi.mocked(observeReleases).mockReturnValue(subject.asObservable())
|
|
145
|
+
|
|
146
|
+
const active = getActiveReleasesState(instance, {
|
|
147
|
+
resource: {projectId: 'test', dataset: 'test'},
|
|
148
|
+
})
|
|
149
|
+
const all = getAllReleasesState(instance, {resource: {projectId: 'test', dataset: 'test'}})
|
|
150
|
+
|
|
151
|
+
const releases: ReleaseDocument[] = [
|
|
152
|
+
{
|
|
153
|
+
_id: 'r-active',
|
|
154
|
+
_type: 'system.release',
|
|
155
|
+
name: 'r-active',
|
|
156
|
+
state: 'active',
|
|
157
|
+
metadata: {releaseType: 'asap'},
|
|
158
|
+
} as ReleaseDocument,
|
|
159
|
+
{
|
|
160
|
+
_id: 'r-archived',
|
|
161
|
+
_type: 'system.release',
|
|
162
|
+
name: 'r-archived',
|
|
163
|
+
state: 'archived',
|
|
164
|
+
metadata: {releaseType: 'asap'},
|
|
165
|
+
} as ReleaseDocument,
|
|
166
|
+
{
|
|
167
|
+
_id: 'r-published',
|
|
168
|
+
_type: 'system.release',
|
|
169
|
+
name: 'r-published',
|
|
170
|
+
state: 'published',
|
|
171
|
+
metadata: {releaseType: 'asap'},
|
|
172
|
+
} as ReleaseDocument,
|
|
173
|
+
]
|
|
174
|
+
|
|
175
|
+
subject.next(releases)
|
|
176
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
177
|
+
|
|
178
|
+
const activeNames = active.getCurrent()?.map((r) => r.name) ?? []
|
|
179
|
+
const allNames = all.getCurrent()?.map((r) => r.name) ?? []
|
|
180
|
+
|
|
181
|
+
expect(activeNames).toEqual(['r-active'])
|
|
182
|
+
expect(allNames).toEqual(expect.arrayContaining(['r-active', 'r-archived', 'r-published']))
|
|
183
|
+
expect(allNames).toHaveLength(3)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('surfaces CORS errors from the live connection as a store-wide error', async () => {
|
|
171
187
|
const subject = new Subject<ReleaseDocument[]>()
|
|
172
|
-
vi.mocked(
|
|
173
|
-
subscribe: () => () => {},
|
|
174
|
-
getCurrent: () => undefined,
|
|
175
|
-
observable: subject.asObservable(),
|
|
176
|
-
} as StateSource<ReleaseDocument[] | undefined>)
|
|
188
|
+
vi.mocked(observeReleases).mockReturnValue(subject.asObservable())
|
|
177
189
|
|
|
178
190
|
const state = getActiveReleasesState(instance, {resource: {projectId: 'test', dataset: 'test'}})
|
|
179
191
|
|
|
180
|
-
subject.
|
|
192
|
+
subject.next([
|
|
193
|
+
{
|
|
194
|
+
_id: 'r1',
|
|
195
|
+
_type: 'system.release',
|
|
196
|
+
name: 'r1',
|
|
197
|
+
state: 'active',
|
|
198
|
+
metadata: {releaseType: 'asap'},
|
|
199
|
+
} as ReleaseDocument,
|
|
200
|
+
])
|
|
201
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
202
|
+
expect(state.getCurrent()).toHaveLength(1)
|
|
203
|
+
|
|
204
|
+
const [, options] = vi.mocked(observeReleases).mock.lastCall!
|
|
205
|
+
const corsError = new Error('CORS misconfiguration')
|
|
206
|
+
options.onCorsError(corsError)
|
|
207
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
208
|
+
|
|
209
|
+
// the CORS error is recorded as store state so the releases selector
|
|
210
|
+
// rethrows it (handled by the Cors Error component)
|
|
211
|
+
expect(() => state.getCurrent()).toThrow(corsError)
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('should surface the error when the releases query errors', async () => {
|
|
215
|
+
const subject = new Subject<ReleaseDocument[]>()
|
|
216
|
+
vi.mocked(observeReleases).mockReturnValue(subject.asObservable())
|
|
217
|
+
|
|
218
|
+
const active = getActiveReleasesState(instance, {
|
|
219
|
+
resource: {projectId: 'test', dataset: 'test'},
|
|
220
|
+
})
|
|
221
|
+
const all = getAllReleasesState(instance, {resource: {projectId: 'test', dataset: 'test'}})
|
|
222
|
+
|
|
223
|
+
const error = new Error('Query failed')
|
|
224
|
+
subject.error(error)
|
|
181
225
|
|
|
182
226
|
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
183
227
|
|
|
184
|
-
|
|
228
|
+
// The error must be surfaced (thrown) rather than silently swallowed,
|
|
229
|
+
// otherwise consumers just see an empty list with no signal.
|
|
230
|
+
expect(() => active.getCurrent()).toThrow(error)
|
|
231
|
+
expect(() => all.getCurrent()).toThrow(error)
|
|
185
232
|
})
|
|
186
233
|
})
|
|
@@ -1,43 +1,35 @@
|
|
|
1
|
-
import {type
|
|
1
|
+
import {type ReleaseDocument} from '@sanity/client'
|
|
2
2
|
import {map} from 'rxjs'
|
|
3
3
|
|
|
4
4
|
import {type DocumentResource} from '../config/sanityConfig'
|
|
5
|
-
/*
|
|
6
|
-
* Although this is an import dependency cycle, it is not a logical cycle:
|
|
7
|
-
* 1. releasesStore uses queryStore as a data source
|
|
8
|
-
* 2. queryStore calls getPerspectiveState for computing release perspectives
|
|
9
|
-
* 3. getPerspectiveState uses releasesStore as a data source
|
|
10
|
-
* 4. however, queryStore does not use getPerspectiveState for the perspective used in releasesStore ("raw")
|
|
11
|
-
*/
|
|
12
|
-
// eslint-disable-next-line import/no-cycle
|
|
13
|
-
import {getQueryState} from '../query/queryStore'
|
|
14
5
|
import {bindActionByResource, type BoundResourceKey} from '../store/createActionBinder'
|
|
15
6
|
import {type SanityInstance} from '../store/createSanityInstance'
|
|
16
7
|
import {createStateSourceAction, type StateSource} from '../store/createStateSourceAction'
|
|
17
8
|
import {defineStore, type StoreContext} from '../store/defineStore'
|
|
9
|
+
import {observeReleases} from './observeReleases'
|
|
18
10
|
import {sortReleases} from './utils/sortReleases'
|
|
19
11
|
|
|
20
12
|
const ARCHIVED_RELEASE_STATES = ['archived', 'published']
|
|
21
13
|
const STABLE_EMPTY_RELEASES: ReleaseDocument[] = []
|
|
22
14
|
|
|
23
15
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
16
|
+
* Lifecycle states a release document can be in. Mirrors the server's
|
|
17
|
+
* `ReleaseState`.
|
|
18
|
+
* @beta
|
|
26
19
|
*/
|
|
27
|
-
export type
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
}
|
|
20
|
+
export type ReleaseState =
|
|
21
|
+
| 'active'
|
|
22
|
+
| 'archiving'
|
|
23
|
+
| 'unarchiving'
|
|
24
|
+
| 'archived'
|
|
25
|
+
| 'published'
|
|
26
|
+
| 'publishing'
|
|
27
|
+
| 'scheduled'
|
|
28
|
+
| 'scheduling'
|
|
38
29
|
|
|
39
30
|
export interface ReleasesStoreState {
|
|
40
31
|
activeReleases?: ReleaseDocument[]
|
|
32
|
+
allReleases?: ReleaseDocument[]
|
|
41
33
|
error?: unknown
|
|
42
34
|
}
|
|
43
35
|
|
|
@@ -45,6 +37,7 @@ export const releasesStore = defineStore<ReleasesStoreState, BoundResourceKey>({
|
|
|
45
37
|
name: 'Releases',
|
|
46
38
|
getInitialState: (): ReleasesStoreState => ({
|
|
47
39
|
activeReleases: undefined,
|
|
40
|
+
allReleases: undefined,
|
|
48
41
|
}),
|
|
49
42
|
initialize: (context) => {
|
|
50
43
|
const subscription = subscribeToReleases(context)
|
|
@@ -59,7 +52,10 @@ export const releasesStore = defineStore<ReleasesStoreState, BoundResourceKey>({
|
|
|
59
52
|
const _getActiveReleasesState = bindActionByResource(
|
|
60
53
|
releasesStore,
|
|
61
54
|
createStateSourceAction({
|
|
62
|
-
selector: ({state}
|
|
55
|
+
selector: ({state}) => {
|
|
56
|
+
if (state.error) throw state.error
|
|
57
|
+
return state.activeReleases
|
|
58
|
+
},
|
|
63
59
|
}),
|
|
64
60
|
)
|
|
65
61
|
|
|
@@ -69,33 +65,62 @@ const _getActiveReleasesState = bindActionByResource(
|
|
|
69
65
|
*/
|
|
70
66
|
export const getActiveReleasesState = (
|
|
71
67
|
instance: SanityInstance,
|
|
72
|
-
options
|
|
73
|
-
): StateSource<ReleaseDocument[] | undefined> =>
|
|
68
|
+
options?: {resource?: DocumentResource},
|
|
69
|
+
): StateSource<ReleaseDocument[] | undefined> =>
|
|
70
|
+
// bindActionByResource keyFn destructures { resource } from the first param, so pass {} when no options
|
|
71
|
+
_getActiveReleasesState(instance, options ?? {})
|
|
74
72
|
|
|
75
|
-
|
|
73
|
+
/**
|
|
74
|
+
* Get every release in the store, including archived and published.
|
|
75
|
+
* @internal
|
|
76
|
+
*/
|
|
77
|
+
const _getAllReleasesState = bindActionByResource(
|
|
78
|
+
releasesStore,
|
|
79
|
+
createStateSourceAction({
|
|
80
|
+
selector: ({state}) => {
|
|
81
|
+
if (state.error) throw state.error
|
|
82
|
+
return state.allReleases
|
|
83
|
+
},
|
|
84
|
+
}),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get every release in the store, including archived and published.
|
|
89
|
+
* @internal
|
|
90
|
+
*/
|
|
91
|
+
export const getAllReleasesState = (
|
|
92
|
+
instance: SanityInstance,
|
|
93
|
+
options?: {resource?: DocumentResource},
|
|
94
|
+
): StateSource<ReleaseDocument[] | undefined> => _getAllReleasesState(instance, options ?? {})
|
|
76
95
|
|
|
77
96
|
const subscribeToReleases = ({
|
|
78
97
|
instance,
|
|
79
98
|
state,
|
|
80
99
|
key: {resource},
|
|
81
100
|
}: StoreContext<ReleasesStoreState, BoundResourceKey>) => {
|
|
82
|
-
const
|
|
83
|
-
query: RELEASES_QUERY,
|
|
84
|
-
perspective: 'raw',
|
|
101
|
+
const releases$ = observeReleases(instance, {
|
|
85
102
|
resource,
|
|
86
|
-
|
|
103
|
+
// Surface CORS errors as store state instead of erroring the stream so
|
|
104
|
+
// they can be handled by the Cors Error component
|
|
105
|
+
onCorsError: (error) => state.set('setError', {error}),
|
|
87
106
|
})
|
|
88
|
-
return
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
107
|
+
return (
|
|
108
|
+
releases$
|
|
109
|
+
.pipe(
|
|
110
|
+
map((releases) => {
|
|
111
|
+
// logic here mirrors that of studio:
|
|
112
|
+
// https://github.com/sanity-io/sanity/blob/156e8fa482703d99219f08da7bacb384517f1513/packages/sanity/src/core/releases/store/useActiveReleases.ts#L29
|
|
113
|
+
const sorted = sortReleases(releases ?? STABLE_EMPTY_RELEASES).reverse()
|
|
114
|
+
state.set('setReleases', {
|
|
115
|
+
allReleases: sorted,
|
|
116
|
+
activeReleases: sorted.filter(
|
|
117
|
+
(release) => !ARCHIVED_RELEASE_STATES.includes(release.state),
|
|
118
|
+
),
|
|
119
|
+
})
|
|
120
|
+
}),
|
|
121
|
+
)
|
|
122
|
+
// live-connection errors are already retried inside observeLiveEvents;
|
|
123
|
+
// a fetch error is terminal and surfaced via setError
|
|
124
|
+
.subscribe({error: (error) => state.set('setError', {error})})
|
|
125
|
+
)
|
|
101
126
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import {type ReleaseDocument} from '@sanity/client'
|
|
1
2
|
import {describe, expect, it} from 'vitest'
|
|
2
3
|
|
|
3
|
-
import {type ReleaseDocument} from '../releasesStore'
|
|
4
4
|
import {sortReleases} from './sortReleases'
|
|
5
5
|
|
|
6
6
|
// Mock function to create a release document
|
|
@@ -15,7 +15,7 @@ function createReleaseMock(
|
|
|
15
15
|
return {
|
|
16
16
|
_id: id,
|
|
17
17
|
_rev: 'rev',
|
|
18
|
-
_type: 'release',
|
|
18
|
+
_type: 'system.release',
|
|
19
19
|
_createdAt: new Date().toISOString(),
|
|
20
20
|
_updatedAt: new Date().toISOString(),
|
|
21
21
|
state: 'active',
|
|
@@ -333,4 +333,40 @@ describe('sortReleases()', () => {
|
|
|
333
333
|
expect(sorted[idx]['_id']).toContain(expectedName)
|
|
334
334
|
})
|
|
335
335
|
})
|
|
336
|
+
|
|
337
|
+
it('should not throw when a release has no metadata', () => {
|
|
338
|
+
// Releases created without a title/type can come back from the API with no
|
|
339
|
+
// `metadata` object at all. Sorting must not throw on these, otherwise the
|
|
340
|
+
// whole releases subscription errors and the list comes back empty.
|
|
341
|
+
const releaseWithoutMetadata = {
|
|
342
|
+
_id: '_.releases.rnometa',
|
|
343
|
+
_rev: 'rev',
|
|
344
|
+
_type: 'system.release',
|
|
345
|
+
_createdAt: '2024-10-24T00:00:00Z',
|
|
346
|
+
_updatedAt: '2024-10-24T00:00:00Z',
|
|
347
|
+
state: 'active',
|
|
348
|
+
name: 'rnometa',
|
|
349
|
+
} as unknown as ReleaseDocument
|
|
350
|
+
|
|
351
|
+
const releases: ReleaseDocument[] = [
|
|
352
|
+
createReleaseMock({
|
|
353
|
+
_id: '_.releases.rasap1',
|
|
354
|
+
_createdAt: '2024-10-25T00:00:00Z',
|
|
355
|
+
metadata: {releaseType: 'asap'},
|
|
356
|
+
}),
|
|
357
|
+
releaseWithoutMetadata,
|
|
358
|
+
createReleaseMock({
|
|
359
|
+
_id: '_.releases.rundecided1',
|
|
360
|
+
_createdAt: '2024-10-26T00:00:00Z',
|
|
361
|
+
metadata: {releaseType: 'undecided'},
|
|
362
|
+
}),
|
|
363
|
+
]
|
|
364
|
+
|
|
365
|
+
let sorted: ReleaseDocument[] = []
|
|
366
|
+
expect(() => {
|
|
367
|
+
sorted = sortReleases(releases)
|
|
368
|
+
}).not.toThrow()
|
|
369
|
+
expect(sorted).toHaveLength(3)
|
|
370
|
+
expect(sorted.map((r) => r._id)).toContain('_.releases.rnometa')
|
|
371
|
+
})
|
|
336
372
|
})
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {type ReleaseDocument} from '
|
|
1
|
+
import {type ReleaseDocument} from '@sanity/client'
|
|
2
2
|
|
|
3
3
|
// mirrors the order of the releases in the releases list in Studio
|
|
4
4
|
// https://github.com/sanity-io/sanity/blob/main/packages/sanity/src/core/releases/hooks/utils.ts
|
|
@@ -6,25 +6,30 @@ export function sortReleases(releases: ReleaseDocument[] = []): ReleaseDocument[
|
|
|
6
6
|
// The order should always be:
|
|
7
7
|
// [undecided (sortByCreatedAt), scheduled(sortBy publishAt || metadata.intendedPublishAt), asap(sortByCreatedAt)]
|
|
8
8
|
return [...releases].sort((a, b) => {
|
|
9
|
+
// metadata is optional on a release document; guard against releases that
|
|
10
|
+
// have no metadata at all so sorting can't throw and wipe out the list.
|
|
11
|
+
const aType = a.metadata?.releaseType
|
|
12
|
+
const bType = b.metadata?.releaseType
|
|
13
|
+
|
|
9
14
|
// undecided are always first, then by createdAt descending
|
|
10
|
-
if (
|
|
15
|
+
if (aType === 'undecided' && bType !== 'undecided') {
|
|
11
16
|
return -1
|
|
12
17
|
}
|
|
13
|
-
if (
|
|
18
|
+
if (aType !== 'undecided' && bType === 'undecided') {
|
|
14
19
|
return 1
|
|
15
20
|
}
|
|
16
|
-
if (
|
|
21
|
+
if (aType === 'undecided' && bType === 'undecided') {
|
|
17
22
|
// Sort by createdAt
|
|
18
23
|
return new Date(b._createdAt).getTime() - new Date(a._createdAt).getTime()
|
|
19
24
|
}
|
|
20
25
|
|
|
21
26
|
// Scheduled are always at the middle, then by publishAt descending
|
|
22
|
-
if (
|
|
23
|
-
const aPublishAt = a['publishAt'] || a.metadata['intendedPublishAt']
|
|
27
|
+
if (aType === 'scheduled' && bType === 'scheduled') {
|
|
28
|
+
const aPublishAt = a['publishAt'] || a.metadata?.['intendedPublishAt']
|
|
24
29
|
if (!aPublishAt) {
|
|
25
30
|
return 1
|
|
26
31
|
}
|
|
27
|
-
const bPublishAt = b['publishAt'] || b.metadata['intendedPublishAt']
|
|
32
|
+
const bPublishAt = b['publishAt'] || b.metadata?.['intendedPublishAt']
|
|
28
33
|
if (!bPublishAt) {
|
|
29
34
|
return -1
|
|
30
35
|
}
|
|
@@ -32,13 +37,13 @@ export function sortReleases(releases: ReleaseDocument[] = []): ReleaseDocument[
|
|
|
32
37
|
}
|
|
33
38
|
|
|
34
39
|
// ASAP are always last, then by createdAt descending
|
|
35
|
-
if (
|
|
40
|
+
if (aType === 'asap' && bType !== 'asap') {
|
|
36
41
|
return 1
|
|
37
42
|
}
|
|
38
|
-
if (
|
|
43
|
+
if (aType !== 'asap' && bType === 'asap') {
|
|
39
44
|
return -1
|
|
40
45
|
}
|
|
41
|
-
if (
|
|
46
|
+
if (aType === 'asap' && bType === 'asap') {
|
|
42
47
|
// Sort by createdAt
|
|
43
48
|
return new Date(b._createdAt).getTime() - new Date(a._createdAt).getTime()
|
|
44
49
|
}
|