@sanity/sdk 2.11.0 → 2.12.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/_chunks-dts/utils.d.ts +171 -19
- package/dist/_chunks-es/_internal.js +41 -26
- package/dist/_chunks-es/_internal.js.map +1 -1
- package/dist/_chunks-es/createGroqSearchFilter.js +25 -9
- package/dist/_chunks-es/createGroqSearchFilter.js.map +1 -1
- package/dist/_chunks-es/telemetryManager.js +25 -19
- package/dist/_chunks-es/telemetryManager.js.map +1 -1
- package/dist/_chunks-es/version.js +1 -1
- package/dist/_exports/_internal.d.ts +27 -11
- package/dist/index.d.ts +2 -2
- package/dist/index.js +723 -418
- package/dist/index.js.map +1 -1
- package/package.json +16 -16
- package/src/_exports/index.ts +23 -2
- package/src/auth/refreshStampedToken.test.ts +2 -2
- package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +116 -0
- package/src/auth/subscribeToStateAndFetchCurrentUser.ts +27 -9
- package/src/config/sanityConfig.ts +12 -0
- package/src/document/actions.test.ts +112 -1
- package/src/document/actions.ts +148 -1
- package/src/document/applyDocumentActions.ts +4 -3
- package/src/document/documentStore.ts +7 -6
- package/src/document/events.test.ts +57 -2
- package/src/document/events.ts +43 -24
- package/src/document/permissions.ts +1 -1
- package/src/document/processActions/create.ts +135 -0
- package/src/document/processActions/delete.ts +100 -0
- package/src/document/processActions/discard.ts +63 -0
- package/src/document/processActions/edit.ts +141 -0
- package/src/document/processActions/processActions.ts +209 -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 +36 -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 +139 -0
- package/src/document/processActions/unpublish.ts +85 -0
- package/src/document/processActions.test.ts +424 -2
- package/src/document/reducers.ts +41 -6
- package/src/releases/getPerspectiveState.test.ts +1 -1
- package/src/releases/releasesStore.test.ts +50 -1
- package/src/releases/releasesStore.ts +41 -18
- package/src/releases/utils/sortReleases.test.ts +2 -2
- package/src/releases/utils/sortReleases.ts +1 -1
- package/src/telemetry/environment.test.ts +119 -0
- package/src/telemetry/environment.ts +92 -0
- package/src/telemetry/{__telemetry__/sdk.telemetry.ts → events.ts} +9 -9
- package/src/telemetry/initTelemetry.test.ts +240 -16
- package/src/telemetry/initTelemetry.ts +39 -16
- package/src/telemetry/telemetryManager.test.ts +129 -65
- package/src/telemetry/telemetryManager.ts +41 -29
- package/src/document/processActions.ts +0 -735
- package/src/telemetry/devMode.test.ts +0 -60
- package/src/telemetry/devMode.ts +0 -41
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanity/sdk",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.12.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Sanity SDK",
|
|
6
6
|
"keywords": [
|
|
@@ -48,41 +48,41 @@
|
|
|
48
48
|
"browserslist": "extends @sanity/browserslist-config",
|
|
49
49
|
"prettier": "@sanity/prettier-config",
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@sanity/bifur-client": "^0.
|
|
51
|
+
"@sanity/bifur-client": "^1.0.0",
|
|
52
52
|
"@sanity/client": "^7.22.0",
|
|
53
|
-
"@sanity/comlink": "^
|
|
53
|
+
"@sanity/comlink": "^4.0.1",
|
|
54
54
|
"@sanity/diff-match-patch": "^3.2.0",
|
|
55
55
|
"@sanity/diff-patch": "^6.0.0",
|
|
56
56
|
"@sanity/id-utils": "^1.0.0",
|
|
57
|
-
"@sanity/image-url": "^2.
|
|
57
|
+
"@sanity/image-url": "^2.1.1",
|
|
58
58
|
"@sanity/json-match": "^1.0.5",
|
|
59
59
|
"@sanity/message-protocol": "^0.23.0",
|
|
60
60
|
"@sanity/mutate": "^0.16.1",
|
|
61
61
|
"@sanity/telemetry": "^1.1.0",
|
|
62
|
-
"@sanity/types": "^5.
|
|
62
|
+
"@sanity/types": "^5.26.0",
|
|
63
63
|
"groq": "3.88.1-typegen-experimental.0",
|
|
64
64
|
"groq-js": "^1.30.1",
|
|
65
65
|
"reselect": "^5.1.1",
|
|
66
66
|
"rxjs": "^7.8.2",
|
|
67
|
-
"zustand": "^5.0.
|
|
67
|
+
"zustand": "^5.0.13"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
70
|
"@sanity/browserslist-config": "^1.0.5",
|
|
71
71
|
"@sanity/pkg-utils": "^8.1.29",
|
|
72
72
|
"@sanity/prettier-config": "^1.0.6",
|
|
73
|
-
"@types/node": "^
|
|
74
|
-
"@vitest/coverage-v8": "4.1.
|
|
75
|
-
"eslint": "^9.
|
|
76
|
-
"prettier": "^3.
|
|
73
|
+
"@types/node": "^24.12.4",
|
|
74
|
+
"@vitest/coverage-v8": "4.1.6",
|
|
75
|
+
"eslint": "^9.39.4",
|
|
76
|
+
"prettier": "^3.8.3",
|
|
77
77
|
"rollup-plugin-visualizer": "^5.14.0",
|
|
78
|
-
"typescript": "^5.
|
|
79
|
-
"vite": "^7.
|
|
80
|
-
"vitest": "^4.1.
|
|
81
|
-
"@repo/config-eslint": "0.0.0",
|
|
78
|
+
"typescript": "^5.9.3",
|
|
79
|
+
"vite": "^7.3.3",
|
|
80
|
+
"vitest": "^4.1.6",
|
|
82
81
|
"@repo/package.bundle": "3.82.0",
|
|
83
|
-
"@repo/config-test": "0.0.1",
|
|
84
82
|
"@repo/package.config": "0.0.1",
|
|
85
|
-
"@repo/tsconfig": "0.0.1"
|
|
83
|
+
"@repo/tsconfig": "0.0.1",
|
|
84
|
+
"@repo/config-test": "0.0.1",
|
|
85
|
+
"@repo/config-eslint": "0.0.0"
|
|
86
86
|
},
|
|
87
87
|
"publishConfig": {
|
|
88
88
|
"access": "public"
|
package/src/_exports/index.ts
CHANGED
|
@@ -105,6 +105,7 @@ export {
|
|
|
105
105
|
type MediaLibrarySource,
|
|
106
106
|
type PerspectiveHandle,
|
|
107
107
|
type ProjectHandle,
|
|
108
|
+
type ReleaseHandle,
|
|
108
109
|
type ReleasePerspective,
|
|
109
110
|
type SanityConfig,
|
|
110
111
|
type StudioConfig,
|
|
@@ -112,19 +113,37 @@ export {
|
|
|
112
113
|
} from '../config/sanityConfig'
|
|
113
114
|
export {getDatasetsState, resolveDatasets} from '../datasets/datasets'
|
|
114
115
|
export {
|
|
116
|
+
type Action,
|
|
117
|
+
archiveRelease,
|
|
118
|
+
type ArchiveReleaseAction,
|
|
115
119
|
createDocument,
|
|
116
120
|
type CreateDocumentAction,
|
|
121
|
+
createRelease,
|
|
122
|
+
type CreateReleaseAction,
|
|
117
123
|
deleteDocument,
|
|
118
124
|
type DeleteDocumentAction,
|
|
125
|
+
deleteRelease,
|
|
126
|
+
type DeleteReleaseAction,
|
|
119
127
|
discardDocument,
|
|
120
128
|
type DiscardDocumentAction,
|
|
121
129
|
type DocumentAction,
|
|
122
130
|
editDocument,
|
|
123
131
|
type EditDocumentAction,
|
|
132
|
+
editRelease,
|
|
133
|
+
type EditReleaseAction,
|
|
124
134
|
publishDocument,
|
|
125
135
|
type PublishDocumentAction,
|
|
136
|
+
publishRelease,
|
|
137
|
+
type PublishReleaseAction,
|
|
138
|
+
type ReleaseAction,
|
|
139
|
+
scheduleRelease,
|
|
140
|
+
type ScheduleReleaseAction,
|
|
141
|
+
unarchiveRelease,
|
|
142
|
+
type UnarchiveReleaseAction,
|
|
126
143
|
unpublishDocument,
|
|
127
144
|
type UnpublishDocumentAction,
|
|
145
|
+
unscheduleRelease,
|
|
146
|
+
type UnscheduleReleaseAction,
|
|
128
147
|
} from '../document/actions'
|
|
129
148
|
export {
|
|
130
149
|
type ActionsResult,
|
|
@@ -155,6 +174,7 @@ export {
|
|
|
155
174
|
} from '../document/events'
|
|
156
175
|
export {type JsonMatch} from '../document/patchOperations'
|
|
157
176
|
export {type DocumentPermissionsResult, type PermissionDeniedReason} from '../document/permissions'
|
|
177
|
+
export {getReleaseDocumentId} from '../document/processActions/releaseUtil'
|
|
158
178
|
export type {FavoriteStatusResponse} from '../favorites/favorites'
|
|
159
179
|
export {getFavoritesState, resolveFavoritesState} from '../favorites/favorites'
|
|
160
180
|
export {
|
|
@@ -214,8 +234,8 @@ export {
|
|
|
214
234
|
resolveQuery,
|
|
215
235
|
} from '../query/queryStore'
|
|
216
236
|
export {getPerspectiveState} from '../releases/getPerspectiveState'
|
|
217
|
-
export type {
|
|
218
|
-
export {getActiveReleasesState} from '../releases/releasesStore'
|
|
237
|
+
export type {ReleaseState} from '../releases/releasesStore'
|
|
238
|
+
export {getActiveReleasesState, getAllReleasesState} from '../releases/releasesStore'
|
|
219
239
|
export {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
|
|
220
240
|
export {type Selector, type StateSource} from '../store/createStateSourceAction'
|
|
221
241
|
export {getUsersKey, parseUsersKey} from '../users/reducers'
|
|
@@ -244,6 +264,7 @@ export {defineIntent, type Intent, type IntentFilter} from '../utils/defineInten
|
|
|
244
264
|
export {getCorsErrorProjectId} from '../utils/getCorsErrorProjectId'
|
|
245
265
|
export {isImportError} from '../utils/isImportError'
|
|
246
266
|
export {CORE_SDK_VERSION} from '../version'
|
|
267
|
+
export type {ReleaseDocument} from '@sanity/client'
|
|
247
268
|
export {
|
|
248
269
|
getIndexForKey,
|
|
249
270
|
getPathDepth,
|
|
@@ -59,8 +59,8 @@ describe('refreshStampedToken', () => {
|
|
|
59
59
|
request: vi.fn(
|
|
60
60
|
async (
|
|
61
61
|
_name: string,
|
|
62
|
-
_options: LockOptions | LockGrantedCallback
|
|
63
|
-
callback?: LockGrantedCallback
|
|
62
|
+
_options: LockOptions | LockGrantedCallback<unknown>,
|
|
63
|
+
callback?: LockGrantedCallback<unknown>,
|
|
64
64
|
) => {
|
|
65
65
|
const actualCallback = typeof _options === 'function' ? _options : callback
|
|
66
66
|
if (!actualCallback) return false
|
|
@@ -112,4 +112,120 @@ describe('subscribeToStateAndFetchCurrentUser', () => {
|
|
|
112
112
|
|
|
113
113
|
subscription.unsubscribe()
|
|
114
114
|
})
|
|
115
|
+
|
|
116
|
+
it('recovers from a fetch error when a new token is set', () => {
|
|
117
|
+
const error = new Error('Unauthorized')
|
|
118
|
+
const mockUser = {id: 'recovered-user'} as CurrentUser
|
|
119
|
+
const mockRequest = vi
|
|
120
|
+
.fn()
|
|
121
|
+
.mockReturnValueOnce(throwError(() => error))
|
|
122
|
+
.mockReturnValueOnce(of(mockUser))
|
|
123
|
+
const mockClient = {observable: {request: mockRequest}}
|
|
124
|
+
const clientFactory = vi.fn().mockReturnValue(mockClient)
|
|
125
|
+
const instance = createSanityInstance({projectId: 'p', dataset: 'd', auth: {clientFactory}})
|
|
126
|
+
|
|
127
|
+
const state = createStoreState(authStore.getInitialState(instance, null))
|
|
128
|
+
const subscription = subscribeToStateAndFetchCurrentUser({state, instance, key: null})
|
|
129
|
+
|
|
130
|
+
// First token causes a 401 — state should transition to ERROR
|
|
131
|
+
state.set('setLoggedIn', {
|
|
132
|
+
authState: {type: AuthStateType.LOGGED_IN, token: 'expired-token', currentUser: null},
|
|
133
|
+
})
|
|
134
|
+
expect(state.get()).toMatchObject({authState: {type: AuthStateType.ERROR, error}})
|
|
135
|
+
|
|
136
|
+
// Simulate comlink providing a fresh token (setAuthToken sets LOGGED_IN with new token)
|
|
137
|
+
state.set('setNewToken', {
|
|
138
|
+
authState: {type: AuthStateType.LOGGED_IN, token: 'fresh-token', currentUser: null},
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
// Subscription should still be alive — re-fetches /users/me with the new token
|
|
142
|
+
expect(state.get()).toMatchObject({
|
|
143
|
+
authState: {type: AuthStateType.LOGGED_IN, token: 'fresh-token', currentUser: mockUser},
|
|
144
|
+
})
|
|
145
|
+
expect(mockRequest).toHaveBeenCalledTimes(2)
|
|
146
|
+
|
|
147
|
+
subscription.unsubscribe()
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('recovers from multiple consecutive fetch errors', () => {
|
|
151
|
+
const error1 = new Error('Unauthorized')
|
|
152
|
+
const error2 = new Error('Unauthorized again')
|
|
153
|
+
const mockUser = {id: 'finally-recovered'} as CurrentUser
|
|
154
|
+
const mockRequest = vi
|
|
155
|
+
.fn()
|
|
156
|
+
.mockReturnValueOnce(throwError(() => error1))
|
|
157
|
+
.mockReturnValueOnce(throwError(() => error2))
|
|
158
|
+
.mockReturnValueOnce(of(mockUser))
|
|
159
|
+
const mockClient = {observable: {request: mockRequest}}
|
|
160
|
+
const clientFactory = vi.fn().mockReturnValue(mockClient)
|
|
161
|
+
const instance = createSanityInstance({projectId: 'p', dataset: 'd', auth: {clientFactory}})
|
|
162
|
+
|
|
163
|
+
const state = createStoreState(authStore.getInitialState(instance, null))
|
|
164
|
+
const subscription = subscribeToStateAndFetchCurrentUser({state, instance, key: null})
|
|
165
|
+
|
|
166
|
+
// First attempt fails
|
|
167
|
+
state.set('setLoggedIn', {
|
|
168
|
+
authState: {type: AuthStateType.LOGGED_IN, token: 'token-1', currentUser: null},
|
|
169
|
+
})
|
|
170
|
+
expect(state.get()).toMatchObject({authState: {type: AuthStateType.ERROR, error: error1}})
|
|
171
|
+
|
|
172
|
+
// Second attempt also fails
|
|
173
|
+
state.set('setNewToken', {
|
|
174
|
+
authState: {type: AuthStateType.LOGGED_IN, token: 'token-2', currentUser: null},
|
|
175
|
+
})
|
|
176
|
+
expect(state.get()).toMatchObject({authState: {type: AuthStateType.ERROR, error: error2}})
|
|
177
|
+
|
|
178
|
+
// Third attempt succeeds
|
|
179
|
+
state.set('setNewToken', {
|
|
180
|
+
authState: {type: AuthStateType.LOGGED_IN, token: 'token-3', currentUser: null},
|
|
181
|
+
})
|
|
182
|
+
expect(state.get()).toMatchObject({
|
|
183
|
+
authState: {type: AuthStateType.LOGGED_IN, token: 'token-3', currentUser: mockUser},
|
|
184
|
+
})
|
|
185
|
+
expect(mockRequest).toHaveBeenCalledTimes(3)
|
|
186
|
+
|
|
187
|
+
subscription.unsubscribe()
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('does not re-fetch with the same token but recovers with a different token', () => {
|
|
191
|
+
const error = new Error('Unauthorized')
|
|
192
|
+
const mockUser = {id: 'recovered-user'} as CurrentUser
|
|
193
|
+
const mockRequest = vi
|
|
194
|
+
.fn()
|
|
195
|
+
.mockReturnValueOnce(throwError(() => error))
|
|
196
|
+
.mockReturnValueOnce(of(mockUser))
|
|
197
|
+
const mockClient = {observable: {request: mockRequest}}
|
|
198
|
+
const clientFactory = vi.fn().mockReturnValue(mockClient)
|
|
199
|
+
const instance = createSanityInstance({projectId: 'p', dataset: 'd', auth: {clientFactory}})
|
|
200
|
+
|
|
201
|
+
const state = createStoreState(authStore.getInitialState(instance, null))
|
|
202
|
+
const subscription = subscribeToStateAndFetchCurrentUser({state, instance, key: null})
|
|
203
|
+
|
|
204
|
+
// First attempt fails
|
|
205
|
+
state.set('setLoggedIn', {
|
|
206
|
+
authState: {type: AuthStateType.LOGGED_IN, token: 'same-token', currentUser: null},
|
|
207
|
+
})
|
|
208
|
+
expect(state.get()).toMatchObject({authState: {type: AuthStateType.ERROR, error}})
|
|
209
|
+
|
|
210
|
+
// Same token should be blocked by distinctUntilChanged — no re-fetch
|
|
211
|
+
state.set('setNewToken', {
|
|
212
|
+
authState: {type: AuthStateType.LOGGED_IN, token: 'same-token', currentUser: null},
|
|
213
|
+
})
|
|
214
|
+
expect(mockRequest).toHaveBeenCalledTimes(1)
|
|
215
|
+
|
|
216
|
+
// A different token should pass distinctUntilChanged and trigger recovery
|
|
217
|
+
state.set('setNewToken', {
|
|
218
|
+
authState: {type: AuthStateType.LOGGED_IN, token: 'different-token', currentUser: null},
|
|
219
|
+
})
|
|
220
|
+
expect(state.get()).toMatchObject({
|
|
221
|
+
authState: {
|
|
222
|
+
type: AuthStateType.LOGGED_IN,
|
|
223
|
+
token: 'different-token',
|
|
224
|
+
currentUser: mockUser,
|
|
225
|
+
},
|
|
226
|
+
})
|
|
227
|
+
expect(mockRequest).toHaveBeenCalledTimes(2)
|
|
228
|
+
|
|
229
|
+
subscription.unsubscribe()
|
|
230
|
+
})
|
|
115
231
|
})
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import {type CurrentUser} from '@sanity/types'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
catchError,
|
|
4
|
+
distinctUntilChanged,
|
|
5
|
+
EMPTY,
|
|
6
|
+
filter,
|
|
7
|
+
map,
|
|
8
|
+
type Subscription,
|
|
9
|
+
switchMap,
|
|
10
|
+
} from 'rxjs'
|
|
3
11
|
|
|
4
12
|
import {type StoreContext} from '../store/defineStore'
|
|
5
13
|
import {DEFAULT_API_VERSION, REQUEST_TAG_PREFIX} from './authConstants'
|
|
@@ -60,11 +68,24 @@ export const subscribeToStateAndFetchCurrentUser = (
|
|
|
60
68
|
}),
|
|
61
69
|
),
|
|
62
70
|
switchMap((client) =>
|
|
63
|
-
client.observable
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
71
|
+
client.observable
|
|
72
|
+
.request<CurrentUser>({
|
|
73
|
+
uri: '/users/me',
|
|
74
|
+
method: 'GET',
|
|
75
|
+
tag: 'users.get-current',
|
|
76
|
+
})
|
|
77
|
+
.pipe(
|
|
78
|
+
/**
|
|
79
|
+
* Catch inside switchMap so the outer subscription survives.
|
|
80
|
+
* Without this, a 401 terminates the subscription permanently
|
|
81
|
+
* and subsequent token refreshes via comlink never re-fetch /users/me.
|
|
82
|
+
* @see SDK-1409
|
|
83
|
+
*/
|
|
84
|
+
catchError((error) => {
|
|
85
|
+
state.set('setError', {authState: {type: AuthStateType.ERROR, error}})
|
|
86
|
+
return EMPTY
|
|
87
|
+
}),
|
|
88
|
+
),
|
|
68
89
|
),
|
|
69
90
|
)
|
|
70
91
|
|
|
@@ -77,8 +98,5 @@ export const subscribeToStateAndFetchCurrentUser = (
|
|
|
77
98
|
: prev.authState,
|
|
78
99
|
}))
|
|
79
100
|
},
|
|
80
|
-
error: (error) => {
|
|
81
|
-
state.set('setError', {authState: {type: AuthStateType.ERROR, error}})
|
|
82
|
-
},
|
|
83
101
|
})
|
|
84
102
|
}
|
|
@@ -118,6 +118,18 @@ export interface DocumentHandle<
|
|
|
118
118
|
documentId: string
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Identifies a release within a Sanity dataset and project. `releaseId` is the
|
|
123
|
+
* `name` parameter on the release document (e.g. `{name: 'r41035a4'}`).
|
|
124
|
+
* The underlying release document ID is `_.releases.<releaseId>`.
|
|
125
|
+
* It's also the `id` parameter sent to the Actions API.
|
|
126
|
+
* (This type doesn't need to have ProjectId / Dataset generics since it's always the same shape)
|
|
127
|
+
* @beta
|
|
128
|
+
*/
|
|
129
|
+
export interface ReleaseHandle extends DatasetHandle {
|
|
130
|
+
releaseId: string
|
|
131
|
+
}
|
|
132
|
+
|
|
121
133
|
/**
|
|
122
134
|
* Represents the complete configuration for a Sanity SDK instance
|
|
123
135
|
* @public
|
|
@@ -1,15 +1,24 @@
|
|
|
1
|
+
import {type ReleaseDocument} from '@sanity/client'
|
|
1
2
|
import {at, patch, set, setIfMissing} from '@sanity/mutate'
|
|
2
3
|
import {type PatchOperations} from '@sanity/types'
|
|
3
4
|
import {describe, expect, it} from 'vitest'
|
|
4
5
|
|
|
5
|
-
import {type DocumentHandle} from '../config/sanityConfig'
|
|
6
|
+
import {type DocumentHandle, type ReleaseHandle} from '../config/sanityConfig'
|
|
6
7
|
import {
|
|
8
|
+
archiveRelease,
|
|
7
9
|
createDocument,
|
|
10
|
+
createRelease,
|
|
8
11
|
deleteDocument,
|
|
12
|
+
deleteRelease,
|
|
9
13
|
discardDocument,
|
|
10
14
|
editDocument,
|
|
15
|
+
editRelease,
|
|
11
16
|
publishDocument,
|
|
17
|
+
publishRelease,
|
|
18
|
+
scheduleRelease,
|
|
19
|
+
unarchiveRelease,
|
|
12
20
|
unpublishDocument,
|
|
21
|
+
unscheduleRelease,
|
|
13
22
|
} from '../document/actions'
|
|
14
23
|
|
|
15
24
|
const dummyPatch: PatchOperations = {
|
|
@@ -19,6 +28,9 @@ const dummyPatch: PatchOperations = {
|
|
|
19
28
|
const dummyDocHandle: DocumentHandle = {documentId: 'drafts.abc123', documentType: 'testType'}
|
|
20
29
|
const dummyDocString = {documentId: 'drafts.abc123', documentType: 'testType'}
|
|
21
30
|
|
|
31
|
+
const dummyReleaseHandle: ReleaseHandle = {releaseId: 'my-release'}
|
|
32
|
+
const dummyReleasePatch: PatchOperations = {set: {'metadata.title': 'Updated title'}}
|
|
33
|
+
|
|
22
34
|
describe('document actions', () => {
|
|
23
35
|
describe('createDocument', () => {
|
|
24
36
|
it('creates a document action from a document handle', () => {
|
|
@@ -207,3 +219,102 @@ describe('document actions', () => {
|
|
|
207
219
|
})
|
|
208
220
|
})
|
|
209
221
|
})
|
|
222
|
+
|
|
223
|
+
describe('release actions', () => {
|
|
224
|
+
describe('createRelease', () => {
|
|
225
|
+
it('creates a release action from a release handle', () => {
|
|
226
|
+
const action = createRelease(dummyReleaseHandle)
|
|
227
|
+
expect(action).toEqual({
|
|
228
|
+
type: 'release.create',
|
|
229
|
+
releaseId: 'my-release',
|
|
230
|
+
metadata: {releaseType: 'undecided'},
|
|
231
|
+
})
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it('creates a release action with metadata', () => {
|
|
235
|
+
const metadata: ReleaseDocument['metadata'] = {
|
|
236
|
+
title: 'My release',
|
|
237
|
+
description: 'Some description',
|
|
238
|
+
releaseType: 'scheduled',
|
|
239
|
+
intendedPublishAt: '2026-01-01T00:00:00.000Z',
|
|
240
|
+
}
|
|
241
|
+
const action = createRelease(dummyReleaseHandle, metadata)
|
|
242
|
+
expect(action).toEqual({
|
|
243
|
+
type: 'release.create',
|
|
244
|
+
releaseId: 'my-release',
|
|
245
|
+
metadata,
|
|
246
|
+
})
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
it('preserves resource fields from the handle', () => {
|
|
250
|
+
const action = createRelease({
|
|
251
|
+
releaseId: 'my-release',
|
|
252
|
+
resource: {dataset: 'production', projectId: 'abc123'},
|
|
253
|
+
})
|
|
254
|
+
expect(action).toEqual({
|
|
255
|
+
type: 'release.create',
|
|
256
|
+
releaseId: 'my-release',
|
|
257
|
+
resource: {dataset: 'production', projectId: 'abc123'},
|
|
258
|
+
metadata: {releaseType: 'undecided'},
|
|
259
|
+
})
|
|
260
|
+
})
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
describe('editRelease', () => {
|
|
264
|
+
it('creates a release.edit action with a patch', () => {
|
|
265
|
+
const action = editRelease(dummyReleaseHandle, dummyReleasePatch)
|
|
266
|
+
expect(action).toEqual({
|
|
267
|
+
type: 'release.edit',
|
|
268
|
+
releaseId: 'my-release',
|
|
269
|
+
patch: dummyReleasePatch,
|
|
270
|
+
})
|
|
271
|
+
})
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
describe('publishRelease', () => {
|
|
275
|
+
it('creates a release.publish action from a release handle', () => {
|
|
276
|
+
const action = publishRelease(dummyReleaseHandle)
|
|
277
|
+
expect(action).toEqual({type: 'release.publish', releaseId: 'my-release'})
|
|
278
|
+
})
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
describe('scheduleRelease', () => {
|
|
282
|
+
it('creates a release.schedule action with publishAt', () => {
|
|
283
|
+
const publishAt = '2026-01-01T00:00:00.000Z'
|
|
284
|
+
const action = scheduleRelease(dummyReleaseHandle, publishAt)
|
|
285
|
+
expect(action).toEqual({
|
|
286
|
+
type: 'release.schedule',
|
|
287
|
+
releaseId: 'my-release',
|
|
288
|
+
publishAt,
|
|
289
|
+
})
|
|
290
|
+
})
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
describe('unscheduleRelease', () => {
|
|
294
|
+
it('creates a release.unschedule action from a release handle', () => {
|
|
295
|
+
const action = unscheduleRelease(dummyReleaseHandle)
|
|
296
|
+
expect(action).toEqual({type: 'release.unschedule', releaseId: 'my-release'})
|
|
297
|
+
})
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
describe('archiveRelease', () => {
|
|
301
|
+
it('creates a release.archive action from a release handle', () => {
|
|
302
|
+
const action = archiveRelease(dummyReleaseHandle)
|
|
303
|
+
expect(action).toEqual({type: 'release.archive', releaseId: 'my-release'})
|
|
304
|
+
})
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
describe('unarchiveRelease', () => {
|
|
308
|
+
it('creates a release.unarchive action from a release handle', () => {
|
|
309
|
+
const action = unarchiveRelease(dummyReleaseHandle)
|
|
310
|
+
expect(action).toEqual({type: 'release.unarchive', releaseId: 'my-release'})
|
|
311
|
+
})
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
describe('deleteRelease', () => {
|
|
315
|
+
it('creates a release.delete action from a release handle', () => {
|
|
316
|
+
const action = deleteRelease(dummyReleaseHandle)
|
|
317
|
+
expect(action).toEqual({type: 'release.delete', releaseId: 'my-release'})
|
|
318
|
+
})
|
|
319
|
+
})
|
|
320
|
+
})
|
package/src/document/actions.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
+
import {type ReleaseDocument} from '@sanity/client'
|
|
1
2
|
import {SanityEncoder} from '@sanity/mutate'
|
|
2
3
|
import {type PatchMutation as SanityMutatePatchMutation} from '@sanity/mutate/_unstable_store'
|
|
3
4
|
import {type PatchMutation, type PatchOperations} from '@sanity/types'
|
|
4
5
|
import {type SanityDocument} from 'groq'
|
|
5
6
|
|
|
6
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
type DocumentHandle,
|
|
9
|
+
type DocumentTypeHandle,
|
|
10
|
+
type ReleaseHandle,
|
|
11
|
+
} from '../config/sanityConfig'
|
|
7
12
|
import {getEffectiveDocumentId} from './util'
|
|
8
13
|
|
|
9
14
|
const isSanityMutatePatch = (value: unknown): value is SanityMutatePatchMutation => {
|
|
@@ -120,6 +125,17 @@ export type DocumentAction<
|
|
|
120
125
|
| UnpublishDocumentAction<TDocumentType, TDataset, TProjectId>
|
|
121
126
|
| DiscardDocumentAction<TDocumentType, TDataset, TProjectId>
|
|
122
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Union of every action accepted by `applyDocumentActions` — both document-
|
|
130
|
+
* level actions and release-lifecycle actions.
|
|
131
|
+
* @beta
|
|
132
|
+
*/
|
|
133
|
+
export type Action<
|
|
134
|
+
TDocumentType extends string = string,
|
|
135
|
+
TDataset extends string = string,
|
|
136
|
+
TProjectId extends string = string,
|
|
137
|
+
> = DocumentAction<TDocumentType, TDataset, TProjectId> | ReleaseAction
|
|
138
|
+
|
|
123
139
|
/**
|
|
124
140
|
* Creates a `CreateDocumentAction` object.
|
|
125
141
|
* @param doc - A handle identifying the document type, dataset, and project. An optional `documentId` can be provided.
|
|
@@ -316,3 +332,134 @@ export function discardDocument<
|
|
|
316
332
|
documentId: effectiveDocumentId,
|
|
317
333
|
}
|
|
318
334
|
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Creates a new release. The `releaseId` must be unique within the current
|
|
338
|
+
* retention period.
|
|
339
|
+
* @beta
|
|
340
|
+
*/
|
|
341
|
+
export interface CreateReleaseAction extends ReleaseHandle {
|
|
342
|
+
type: 'release.create'
|
|
343
|
+
metadata: ReleaseDocument['metadata']
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Patches the metadata of an existing release.
|
|
348
|
+
* @beta
|
|
349
|
+
*/
|
|
350
|
+
export interface EditReleaseAction extends ReleaseHandle {
|
|
351
|
+
type: 'release.edit'
|
|
352
|
+
patch: PatchOperations
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Publishes all version documents in a release.
|
|
357
|
+
* @beta
|
|
358
|
+
*/
|
|
359
|
+
export interface PublishReleaseAction extends ReleaseHandle {
|
|
360
|
+
type: 'release.publish'
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Schedules a release to be published at the given UTC time. Locks the
|
|
365
|
+
* version documents server-side until the release is unscheduled or published.
|
|
366
|
+
* @beta
|
|
367
|
+
*/
|
|
368
|
+
export interface ScheduleReleaseAction extends ReleaseHandle {
|
|
369
|
+
type: 'release.schedule'
|
|
370
|
+
publishAt: string
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Unschedules a release that was previously scheduled, returning it to the
|
|
375
|
+
* active editable state.
|
|
376
|
+
* @beta
|
|
377
|
+
*/
|
|
378
|
+
export interface UnscheduleReleaseAction extends ReleaseHandle {
|
|
379
|
+
type: 'release.unschedule'
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Archives an active release. Version documents within the release are
|
|
384
|
+
* removed and no longer queryable, though still recoverable through history
|
|
385
|
+
* during the retention period.
|
|
386
|
+
* @beta
|
|
387
|
+
*/
|
|
388
|
+
export interface ArchiveReleaseAction extends ReleaseHandle {
|
|
389
|
+
type: 'release.archive'
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Restores an archived release. Only possible during the retention period.
|
|
394
|
+
* @beta
|
|
395
|
+
*/
|
|
396
|
+
export interface UnarchiveReleaseAction extends ReleaseHandle {
|
|
397
|
+
type: 'release.unarchive'
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Permanently deletes an archived or published release. To remove an active
|
|
402
|
+
* release, use the archive action first.
|
|
403
|
+
* @beta
|
|
404
|
+
*/
|
|
405
|
+
export interface DeleteReleaseAction extends ReleaseHandle {
|
|
406
|
+
type: 'release.delete'
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Union of all release actions that can be dispatched alongside document
|
|
411
|
+
* actions through `applyDocumentActions`.
|
|
412
|
+
* @beta
|
|
413
|
+
*/
|
|
414
|
+
export type ReleaseAction =
|
|
415
|
+
| CreateReleaseAction
|
|
416
|
+
| EditReleaseAction
|
|
417
|
+
| PublishReleaseAction
|
|
418
|
+
| ScheduleReleaseAction
|
|
419
|
+
| UnscheduleReleaseAction
|
|
420
|
+
| ArchiveReleaseAction
|
|
421
|
+
| UnarchiveReleaseAction
|
|
422
|
+
| DeleteReleaseAction
|
|
423
|
+
|
|
424
|
+
/** @beta */
|
|
425
|
+
export function createRelease(
|
|
426
|
+
handle: ReleaseHandle,
|
|
427
|
+
metadata: ReleaseDocument['metadata'] = {releaseType: 'undecided'},
|
|
428
|
+
): CreateReleaseAction {
|
|
429
|
+
return {type: 'release.create', ...handle, metadata}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** @beta */
|
|
433
|
+
export function editRelease(handle: ReleaseHandle, patch: PatchOperations): EditReleaseAction {
|
|
434
|
+
return {type: 'release.edit', ...handle, patch}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/** @beta */
|
|
438
|
+
export function publishRelease(handle: ReleaseHandle): PublishReleaseAction {
|
|
439
|
+
return {type: 'release.publish', ...handle}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** @beta */
|
|
443
|
+
export function scheduleRelease(handle: ReleaseHandle, publishAt: string): ScheduleReleaseAction {
|
|
444
|
+
return {type: 'release.schedule', ...handle, publishAt}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/** @beta */
|
|
448
|
+
export function unscheduleRelease(handle: ReleaseHandle): UnscheduleReleaseAction {
|
|
449
|
+
return {type: 'release.unschedule', ...handle}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** @beta */
|
|
453
|
+
export function archiveRelease(handle: ReleaseHandle): ArchiveReleaseAction {
|
|
454
|
+
return {type: 'release.archive', ...handle}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/** @beta */
|
|
458
|
+
export function unarchiveRelease(handle: ReleaseHandle): UnarchiveReleaseAction {
|
|
459
|
+
return {type: 'release.unarchive', ...handle}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** @beta */
|
|
463
|
+
export function deleteRelease(handle: ReleaseHandle): DeleteReleaseAction {
|
|
464
|
+
return {type: 'release.delete', ...handle}
|
|
465
|
+
}
|
|
@@ -5,7 +5,7 @@ import {type DocumentResource} from '../config/sanityConfig'
|
|
|
5
5
|
import {bindActionByResource} from '../store/createActionBinder'
|
|
6
6
|
import {type SanityInstance} from '../store/createSanityInstance'
|
|
7
7
|
import {type StoreContext} from '../store/defineStore'
|
|
8
|
-
import {type
|
|
8
|
+
import {type Action} from './actions'
|
|
9
9
|
import {documentStore, type DocumentStoreState} from './documentStore'
|
|
10
10
|
import {type DocumentTransactionSubmissionResult} from './events'
|
|
11
11
|
import {type DocumentSet} from './processMutations'
|
|
@@ -26,9 +26,10 @@ export interface ActionsResult<TDocument extends SanityDocument = SanityDocument
|
|
|
26
26
|
/** @beta */
|
|
27
27
|
export interface ApplyDocumentActionsOptions {
|
|
28
28
|
/**
|
|
29
|
-
* List of actions to apply.
|
|
29
|
+
* List of actions to apply. Accepts both document actions and release
|
|
30
|
+
* lifecycle actions because they share the same transaction pipeline.
|
|
30
31
|
*/
|
|
31
|
-
actions:
|
|
32
|
+
actions: Action[]
|
|
32
33
|
|
|
33
34
|
/**
|
|
34
35
|
* The resource to which the documents being acted on belong.
|