@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.
Files changed (57) hide show
  1. package/dist/_chunks-dts/utils.d.ts +171 -19
  2. package/dist/_chunks-es/_internal.js +41 -26
  3. package/dist/_chunks-es/_internal.js.map +1 -1
  4. package/dist/_chunks-es/createGroqSearchFilter.js +25 -9
  5. package/dist/_chunks-es/createGroqSearchFilter.js.map +1 -1
  6. package/dist/_chunks-es/telemetryManager.js +25 -19
  7. package/dist/_chunks-es/telemetryManager.js.map +1 -1
  8. package/dist/_chunks-es/version.js +1 -1
  9. package/dist/_exports/_internal.d.ts +27 -11
  10. package/dist/index.d.ts +2 -2
  11. package/dist/index.js +723 -418
  12. package/dist/index.js.map +1 -1
  13. package/package.json +16 -16
  14. package/src/_exports/index.ts +23 -2
  15. package/src/auth/refreshStampedToken.test.ts +2 -2
  16. package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +116 -0
  17. package/src/auth/subscribeToStateAndFetchCurrentUser.ts +27 -9
  18. package/src/config/sanityConfig.ts +12 -0
  19. package/src/document/actions.test.ts +112 -1
  20. package/src/document/actions.ts +148 -1
  21. package/src/document/applyDocumentActions.ts +4 -3
  22. package/src/document/documentStore.ts +7 -6
  23. package/src/document/events.test.ts +57 -2
  24. package/src/document/events.ts +43 -24
  25. package/src/document/permissions.ts +1 -1
  26. package/src/document/processActions/create.ts +135 -0
  27. package/src/document/processActions/delete.ts +100 -0
  28. package/src/document/processActions/discard.ts +63 -0
  29. package/src/document/processActions/edit.ts +141 -0
  30. package/src/document/processActions/processActions.ts +209 -0
  31. package/src/document/processActions/publish.ts +120 -0
  32. package/src/document/processActions/releaseArchive.ts +77 -0
  33. package/src/document/processActions/releaseCreate.ts +59 -0
  34. package/src/document/processActions/releaseDelete.ts +65 -0
  35. package/src/document/processActions/releaseEdit.ts +36 -0
  36. package/src/document/processActions/releasePublish.ts +45 -0
  37. package/src/document/processActions/releaseSchedule.ts +87 -0
  38. package/src/document/processActions/releaseUtil.ts +31 -0
  39. package/src/document/processActions/shared.ts +139 -0
  40. package/src/document/processActions/unpublish.ts +85 -0
  41. package/src/document/processActions.test.ts +424 -2
  42. package/src/document/reducers.ts +41 -6
  43. package/src/releases/getPerspectiveState.test.ts +1 -1
  44. package/src/releases/releasesStore.test.ts +50 -1
  45. package/src/releases/releasesStore.ts +41 -18
  46. package/src/releases/utils/sortReleases.test.ts +2 -2
  47. package/src/releases/utils/sortReleases.ts +1 -1
  48. package/src/telemetry/environment.test.ts +119 -0
  49. package/src/telemetry/environment.ts +92 -0
  50. package/src/telemetry/{__telemetry__/sdk.telemetry.ts → events.ts} +9 -9
  51. package/src/telemetry/initTelemetry.test.ts +240 -16
  52. package/src/telemetry/initTelemetry.ts +39 -16
  53. package/src/telemetry/telemetryManager.test.ts +129 -65
  54. package/src/telemetry/telemetryManager.ts +41 -29
  55. package/src/document/processActions.ts +0 -735
  56. package/src/telemetry/devMode.test.ts +0 -60
  57. 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.11.0",
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.4.1",
51
+ "@sanity/bifur-client": "^1.0.0",
52
52
  "@sanity/client": "^7.22.0",
53
- "@sanity/comlink": "^3.1.1",
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.0.3",
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.2.0",
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.12"
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": "^22.19.1",
74
- "@vitest/coverage-v8": "4.1.5",
75
- "eslint": "^9.22.0",
76
- "prettier": "^3.7.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.8.3",
79
- "vite": "^7.0.0",
80
- "vitest": "^4.1.4",
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"
@@ -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 {ReleaseDocument} from '../releases/releasesStore'
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 {distinctUntilChanged, filter, map, type Subscription, switchMap} from 'rxjs'
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.request<CurrentUser>({
64
- uri: '/users/me',
65
- method: 'GET',
66
- tag: 'users.get-current',
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
+ })
@@ -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 {type DocumentHandle, type DocumentTypeHandle} from '../config/sanityConfig'
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 DocumentAction} from './actions'
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: DocumentAction[]
32
+ actions: Action[]
32
33
 
33
34
  /**
34
35
  * The resource to which the documents being acted on belong.