@sanity/sdk 2.11.1 → 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 (47) 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 +15 -4
  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 +355 -75
  12. package/dist/index.js.map +1 -1
  13. package/package.json +8 -8
  14. package/src/_exports/index.ts +23 -2
  15. package/src/config/sanityConfig.ts +12 -0
  16. package/src/document/actions.test.ts +112 -1
  17. package/src/document/actions.ts +148 -1
  18. package/src/document/applyDocumentActions.ts +4 -3
  19. package/src/document/documentStore.ts +6 -5
  20. package/src/document/events.test.ts +57 -2
  21. package/src/document/events.ts +43 -24
  22. package/src/document/processActions/edit.ts +9 -44
  23. package/src/document/processActions/processActions.ts +44 -3
  24. package/src/document/processActions/releaseArchive.ts +77 -0
  25. package/src/document/processActions/releaseCreate.ts +59 -0
  26. package/src/document/processActions/releaseDelete.ts +65 -0
  27. package/src/document/processActions/releaseEdit.ts +36 -0
  28. package/src/document/processActions/releasePublish.ts +45 -0
  29. package/src/document/processActions/releaseSchedule.ts +87 -0
  30. package/src/document/processActions/releaseUtil.ts +31 -0
  31. package/src/document/processActions/shared.ts +94 -2
  32. package/src/document/processActions.test.ts +423 -1
  33. package/src/document/reducers.ts +40 -5
  34. package/src/releases/getPerspectiveState.test.ts +1 -1
  35. package/src/releases/releasesStore.test.ts +50 -1
  36. package/src/releases/releasesStore.ts +41 -18
  37. package/src/releases/utils/sortReleases.test.ts +2 -2
  38. package/src/releases/utils/sortReleases.ts +1 -1
  39. package/src/telemetry/environment.test.ts +119 -0
  40. package/src/telemetry/environment.ts +92 -0
  41. package/src/telemetry/{__telemetry__/sdk.telemetry.ts → events.ts} +9 -9
  42. package/src/telemetry/initTelemetry.test.ts +240 -16
  43. package/src/telemetry/initTelemetry.ts +39 -16
  44. package/src/telemetry/telemetryManager.test.ts +129 -65
  45. package/src/telemetry/telemetryManager.ts +41 -29
  46. package/src/telemetry/devMode.test.ts +0 -60
  47. 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.1",
3
+ "version": "2.12.0",
4
4
  "private": false,
5
5
  "description": "Sanity SDK",
6
6
  "keywords": [
@@ -59,7 +59,7 @@
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.24.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",
@@ -70,19 +70,19 @@
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": "^24.12.3",
74
- "@vitest/coverage-v8": "4.1.5",
73
+ "@types/node": "^24.12.4",
74
+ "@vitest/coverage-v8": "4.1.6",
75
75
  "eslint": "^9.39.4",
76
76
  "prettier": "^3.8.3",
77
77
  "rollup-plugin-visualizer": "^5.14.0",
78
78
  "typescript": "^5.9.3",
79
79
  "vite": "^7.3.3",
80
- "vitest": "^4.1.5",
81
- "@repo/config-eslint": "0.0.0",
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,
@@ -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.
@@ -60,6 +60,7 @@ import {
60
60
  type Grant,
61
61
  } from './permissions'
62
62
  import {ActionError} from './processActions/processActions'
63
+ import {isReleaseAction} from './processActions/releaseUtil'
63
64
  import {
64
65
  type AppliedTransaction,
65
66
  applyFirstQueuedTransaction,
@@ -414,10 +415,11 @@ const subscribeToAppliedAndSubmitNextTransaction = ({
414
415
  outgoing,
415
416
  }))
416
417
 
417
- // Any liveEdit action in the batch routes to the mutations API. For mixed batches
418
- // non-liveEdit operations (e.g. publish) lose atomicity, but that is acceptable
419
- // given how rare mixed batches are.
420
- if (outgoing.actions.some((action) => action.liveEdit)) {
418
+ // liveEdit transactions route to the mutations API; everything else routes
419
+ // to the actions API. processActions rejects transactions that mix the two,
420
+ // and reducers won't batch across that boundary, so a batch is always
421
+ // entirely liveEdit or entirely not.
422
+ if (outgoing.actions.some((action) => !isReleaseAction(action) && action.liveEdit)) {
421
423
  return client.observable
422
424
  .mutate(outgoing.outgoingMutations as Mutation[], {
423
425
  transactionId: outgoing.transactionId,
@@ -430,7 +432,6 @@ const subscribeToAppliedAndSubmitNextTransaction = ({
430
432
  .pipe(revertOnError, toResult)
431
433
  }
432
434
 
433
- // Pure non-liveEdit transactions use the actions API.
434
435
  return client.observable
435
436
  .action(outgoing.outgoingActions as Action[], {
436
437
  transactionId: outgoing.transactionId,
@@ -1,6 +1,6 @@
1
1
  import {describe, expect, it} from 'vitest'
2
2
 
3
- import {type DocumentAction} from '../document/actions'
3
+ import {type Action, type DocumentAction} from '../document/actions'
4
4
  import {type DocumentEvent, getDocumentEvents} from '../document/events'
5
5
  import {type OutgoingTransaction} from '../document/reducers'
6
6
 
@@ -41,11 +41,66 @@ describe('getDocumentEvents', () => {
41
41
  expect(events).toHaveLength(outgoing.actions.length)
42
42
  events.forEach((event) => {
43
43
  const action = outgoing.actions.find(
44
- (a) => 'documentId' in event && event.documentId === a.documentId,
44
+ (a) => 'documentId' in a && 'documentId' in event && event.documentId === a.documentId,
45
45
  )
46
46
  expect(action).toBeDefined()
47
47
  expect(event.type).toEqual(expectedMap[action!.type])
48
48
  expect((event as Extract<DocumentEvent, {outgoing: unknown}>).outgoing).toBe(outgoing)
49
49
  })
50
50
  })
51
+
52
+ it('emits created/edited/deleted events for release.create/edit/delete with release doc IDs', () => {
53
+ const outgoing: OutgoingTransaction = {
54
+ transactionId: 'txn-release',
55
+ actions: [
56
+ {type: 'release.create', releaseId: 'r1'} as Action,
57
+ {type: 'release.edit', releaseId: 'r2', patch: {set: {}}} as Action,
58
+ {type: 'release.delete', releaseId: 'r3'} as Action,
59
+ ],
60
+ disableBatching: false,
61
+ batchedTransactionIds: [],
62
+ outgoingActions: [],
63
+ outgoingMutations: [],
64
+ base: {},
65
+ working: {},
66
+ previous: {},
67
+ previousRevs: {},
68
+ timestamp: '2025-02-06T00:00:00.000Z',
69
+ }
70
+
71
+ const events = getDocumentEvents(outgoing)
72
+ expect(events).toEqual([
73
+ {type: 'created', documentId: '_.releases.r1', outgoing},
74
+ {type: 'edited', documentId: '_.releases.r2', outgoing},
75
+ {type: 'deleted', documentId: '_.releases.r3', outgoing},
76
+ ])
77
+ })
78
+
79
+ it('skips release actions that have no local mutation (publish/schedule/archive/etc.)', () => {
80
+ const outgoing: OutgoingTransaction = {
81
+ transactionId: 'txn-release-noop',
82
+ actions: [
83
+ {type: 'release.publish', releaseId: 'r1'} as Action,
84
+ {
85
+ type: 'release.schedule',
86
+ releaseId: 'r2',
87
+ publishAt: '2026-01-01T00:00:00.000Z',
88
+ } as Action,
89
+ {type: 'release.unschedule', releaseId: 'r3'} as Action,
90
+ {type: 'release.archive', releaseId: 'r4'} as Action,
91
+ {type: 'release.unarchive', releaseId: 'r5'} as Action,
92
+ ],
93
+ disableBatching: false,
94
+ batchedTransactionIds: [],
95
+ outgoingActions: [],
96
+ outgoingMutations: [],
97
+ base: {},
98
+ working: {},
99
+ previous: {},
100
+ previousRevs: {},
101
+ timestamp: '2025-02-06T00:00:00.000Z',
102
+ }
103
+
104
+ expect(getDocumentEvents(outgoing)).toEqual([])
105
+ })
51
106
  })
@@ -1,6 +1,7 @@
1
1
  import {type MultipleMutationResult, type SanityClient} from '@sanity/client'
2
2
 
3
- import {type DocumentAction} from './actions'
3
+ import {type DocumentAction, type ReleaseAction} from './actions'
4
+ import {getReleaseDocumentId, isReleaseAction} from './processActions/releaseUtil'
4
5
  import {type OutgoingTransaction} from './reducers'
5
6
 
6
7
  /** @beta Response body from submitting an outgoing transaction (actions or mutations API). */
@@ -118,31 +119,49 @@ export interface DocumentDiscardedEvent {
118
119
  outgoing: OutgoingTransaction
119
120
  }
120
121
 
121
- export function getDocumentEvents(outgoing: OutgoingTransaction): DocumentEvent[] {
122
- const documentIdsByAction = Object.entries(
123
- outgoing.actions.reduce(
124
- (acc, {type, documentId}) => {
125
- const ids = acc[type] || new Set()
126
- if (documentId) ids.add(documentId)
127
- acc[type] = ids
128
- return acc
129
- },
130
- {} as Record<DocumentAction['type'], Set<string>>,
131
- ),
132
- ) as [DocumentAction['type'], Set<string>][]
122
+ // Release actions that write a mutation to the local release doc map onto
123
+ // the regular per-document events with `documentId = '_.releases.<releaseId>'`.
124
+ // The other release actions (publish/schedule/unschedule/archive/unarchive)
125
+ // don't mutate local state, so they aren't in the map and get skipped — they
126
+ // surface through the transaction-level `accepted`/`reverted` events instead.
127
+ const actionMap = {
128
+ 'document.create': 'created',
129
+ 'document.delete': 'deleted',
130
+ 'document.discard': 'discarded',
131
+ 'document.edit': 'edited',
132
+ 'document.publish': 'published',
133
+ 'document.unpublish': 'unpublished',
134
+ 'release.create': 'created',
135
+ 'release.edit': 'edited',
136
+ 'release.delete': 'deleted',
137
+ } satisfies Partial<Record<DocumentAction['type'] | ReleaseAction['type'], DocumentEvent['type']>>
138
+
139
+ type MappedActionType = keyof typeof actionMap
133
140
 
134
- const actionMap = {
135
- 'document.create': 'created',
136
- 'document.delete': 'deleted',
137
- 'document.discard': 'discarded',
138
- 'document.edit': 'edited',
139
- 'document.publish': 'published',
140
- 'document.unpublish': 'unpublished',
141
- } satisfies Record<DocumentAction['type'], DocumentEvent['type']>
141
+ export function getDocumentEvents(outgoing: OutgoingTransaction): DocumentEvent[] {
142
+ const documentIdsByAction = outgoing.actions.reduce(
143
+ (acc, action) => {
144
+ if (!(action.type in actionMap)) return acc
145
+ const documentId = isReleaseAction(action)
146
+ ? getReleaseDocumentId(action.releaseId)
147
+ : action.documentId
148
+ if (!documentId) return acc
149
+ const type = action.type as MappedActionType
150
+ const ids = acc[type] ?? new Set<string>()
151
+ ids.add(documentId)
152
+ acc[type] = ids
153
+ return acc
154
+ },
155
+ {} as Partial<Record<MappedActionType, Set<string>>>,
156
+ )
142
157
 
143
- return documentIdsByAction.flatMap(([actionType, documentIds]) =>
144
- Array.from(documentIds).map(
145
- (documentId): DocumentEvent => ({type: actionMap[actionType], documentId, outgoing}),
158
+ return Object.entries(documentIdsByAction).flatMap(([actionType, documentIds]) =>
159
+ Array.from(documentIds ?? []).map(
160
+ (documentId): DocumentEvent => ({
161
+ type: actionMap[actionType as MappedActionType],
162
+ documentId,
163
+ outgoing,
164
+ }),
146
165
  ),
147
166
  )
148
167
  }