@sanity/sdk 2.11.1 → 2.13.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 +175 -19
- package/dist/_chunks-es/_internal.js +41 -26
- package/dist/_chunks-es/_internal.js.map +1 -1
- package/dist/_chunks-es/createGroqSearchFilter.js +15 -4
- 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 +465 -131
- package/dist/index.js.map +1 -1
- package/package.json +11 -11
- package/src/_exports/index.ts +23 -2
- 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.test.ts +24 -0
- package/src/document/applyDocumentActions.ts +17 -5
- package/src/document/documentConstants.ts +7 -0
- package/src/document/documentStore.test.ts +69 -0
- package/src/document/documentStore.ts +42 -10
- package/src/document/events.test.ts +57 -2
- package/src/document/events.ts +43 -24
- package/src/document/listen.ts +1 -1
- package/src/document/permissions.test.ts +79 -0
- package/src/document/permissions.ts +8 -7
- package/src/document/processActions/create.ts +7 -4
- package/src/document/processActions/delete.ts +4 -4
- package/src/document/processActions/discard.ts +2 -2
- package/src/document/processActions/edit.ts +13 -47
- package/src/document/processActions/processActions.ts +53 -3
- package/src/document/processActions/publish.ts +4 -4
- 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 +108 -4
- package/src/document/processActions/unpublish.ts +3 -3
- package/src/document/processActions.test.ts +423 -1
- package/src/document/reducers.ts +44 -8
- package/src/document/resourceRules.test.ts +178 -0
- package/src/document/resourceRules.ts +117 -0
- 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/telemetry/devMode.test.ts +0 -60
- package/src/telemetry/devMode.ts +0 -41
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {type EditDocumentAction} from './actions'
|
|
4
|
+
import {getEffectiveDocumentModel, normalizeActionsForResource} from './resourceRules'
|
|
5
|
+
|
|
6
|
+
const datasetResource = {projectId: 'p', dataset: 'd'}
|
|
7
|
+
const canvasResource = {canvasId: 'canvas-1'}
|
|
8
|
+
const mediaLibraryResource = {mediaLibraryId: 'ml-1'}
|
|
9
|
+
|
|
10
|
+
function editAction(
|
|
11
|
+
overrides: Partial<EditDocumentAction> & Pick<EditDocumentAction, 'documentId' | 'documentType'>,
|
|
12
|
+
): EditDocumentAction {
|
|
13
|
+
return {
|
|
14
|
+
type: 'document.edit',
|
|
15
|
+
patches: [{set: {foo: 'bar'}}],
|
|
16
|
+
...overrides,
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('getEffectiveDocumentModel', () => {
|
|
21
|
+
it('returns passthrough for no resource', () => {
|
|
22
|
+
expect(getEffectiveDocumentModel(undefined, 'anything')).toEqual({
|
|
23
|
+
liveEdit: undefined,
|
|
24
|
+
supportsReleases: true,
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('returns passthrough for dataset resources', () => {
|
|
29
|
+
expect(getEffectiveDocumentModel(datasetResource, 'author')).toEqual({
|
|
30
|
+
liveEdit: undefined,
|
|
31
|
+
supportsReleases: true,
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('forces liveEdit and disallows releases for canvas resources', () => {
|
|
36
|
+
expect(getEffectiveDocumentModel(canvasResource, 'page')).toEqual({
|
|
37
|
+
liveEdit: true,
|
|
38
|
+
supportsReleases: false,
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('forces liveEdit for non-asset media library types', () => {
|
|
43
|
+
expect(getEffectiveDocumentModel(mediaLibraryResource, 'sanity.imageAsset')).toEqual({
|
|
44
|
+
liveEdit: true,
|
|
45
|
+
supportsReleases: false,
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('keeps draft/published model for sanity.asset in media library', () => {
|
|
50
|
+
expect(getEffectiveDocumentModel(mediaLibraryResource, 'sanity.asset')).toEqual({
|
|
51
|
+
liveEdit: false,
|
|
52
|
+
supportsReleases: false,
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
describe('normalizeActionsForResource', () => {
|
|
58
|
+
let warnSpy: ReturnType<typeof vi.spyOn>
|
|
59
|
+
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
afterEach(() => {
|
|
65
|
+
warnSpy.mockRestore()
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('leaves dataset edits with a release perspective alone', () => {
|
|
69
|
+
const action = editAction({
|
|
70
|
+
documentId: 'versions.relA.doc1',
|
|
71
|
+
documentType: 'author',
|
|
72
|
+
perspective: {releaseName: 'relA'},
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
const result = normalizeActionsForResource([action], datasetResource)
|
|
76
|
+
|
|
77
|
+
expect(result[0]).toBe(action)
|
|
78
|
+
expect(warnSpy).not.toHaveBeenCalled()
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('strips release perspective and forces liveEdit for canvas, warning once', () => {
|
|
82
|
+
const action = editAction({
|
|
83
|
+
documentId: 'versions.relA.doc1',
|
|
84
|
+
documentType: 'page',
|
|
85
|
+
perspective: {releaseName: 'relA'},
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
const [result] = normalizeActionsForResource([action], canvasResource) as [EditDocumentAction]
|
|
89
|
+
|
|
90
|
+
expect(result.liveEdit).toBe(true)
|
|
91
|
+
expect(result.perspective).toBeUndefined()
|
|
92
|
+
expect(result.documentId).toBe('doc1')
|
|
93
|
+
expect(warnSpy).toHaveBeenCalledTimes(1)
|
|
94
|
+
expect(warnSpy.mock.calls[0][0]).toContain('Canvas')
|
|
95
|
+
expect(warnSpy.mock.calls[0][0]).toContain('page (doc1)')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('strips release perspective and forces liveEdit for non-asset media library types', () => {
|
|
99
|
+
const action = editAction({
|
|
100
|
+
documentId: 'versions.relA.doc1',
|
|
101
|
+
documentType: 'sanity.imageAsset',
|
|
102
|
+
perspective: {releaseName: 'relA'},
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
const [result] = normalizeActionsForResource([action], mediaLibraryResource) as [
|
|
106
|
+
EditDocumentAction,
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
expect(result.liveEdit).toBe(true)
|
|
110
|
+
expect(result.perspective).toBeUndefined()
|
|
111
|
+
expect(result.documentId).toBe('doc1')
|
|
112
|
+
expect(warnSpy).toHaveBeenCalledTimes(1)
|
|
113
|
+
expect(warnSpy.mock.calls[0][0]).toContain('Media Library')
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('strips release perspective but keeps draft/publish model for sanity.asset', () => {
|
|
117
|
+
const action = editAction({
|
|
118
|
+
documentId: 'versions.relA.doc1',
|
|
119
|
+
documentType: 'sanity.asset',
|
|
120
|
+
perspective: {releaseName: 'relA'},
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
const [result] = normalizeActionsForResource([action], mediaLibraryResource) as [
|
|
124
|
+
EditDocumentAction,
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
expect(result.liveEdit).toBeUndefined()
|
|
128
|
+
expect(result.perspective).toBeUndefined()
|
|
129
|
+
expect(result.documentId).toBe('doc1')
|
|
130
|
+
expect(warnSpy).toHaveBeenCalledTimes(1)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('silently forces liveEdit for canvas with no release perspective', () => {
|
|
134
|
+
const action = editAction({
|
|
135
|
+
documentId: 'drafts.doc1',
|
|
136
|
+
documentType: 'page',
|
|
137
|
+
perspective: 'drafts',
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
const [result] = normalizeActionsForResource([action], canvasResource) as [EditDocumentAction]
|
|
141
|
+
|
|
142
|
+
expect(result.liveEdit).toBe(true)
|
|
143
|
+
expect(result.documentId).toBe('doc1')
|
|
144
|
+
expect(warnSpy).not.toHaveBeenCalled()
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('emits a single warning for multiple stripped actions in one call', () => {
|
|
148
|
+
const a = editAction({
|
|
149
|
+
documentId: 'versions.relA.doc1',
|
|
150
|
+
documentType: 'page',
|
|
151
|
+
perspective: {releaseName: 'relA'},
|
|
152
|
+
})
|
|
153
|
+
const b = editAction({
|
|
154
|
+
documentId: 'versions.relA.doc2',
|
|
155
|
+
documentType: 'page',
|
|
156
|
+
perspective: {releaseName: 'relA'},
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
normalizeActionsForResource([a, b], canvasResource)
|
|
160
|
+
|
|
161
|
+
expect(warnSpy).toHaveBeenCalledTimes(1)
|
|
162
|
+
expect(warnSpy.mock.calls[0][0]).toContain('doc1')
|
|
163
|
+
expect(warnSpy.mock.calls[0][0]).toContain('doc2')
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('passes non-edit actions through unchanged', () => {
|
|
167
|
+
const action = {
|
|
168
|
+
type: 'document.publish' as const,
|
|
169
|
+
documentId: 'doc1',
|
|
170
|
+
documentType: 'page',
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const result = normalizeActionsForResource([action], canvasResource)
|
|
174
|
+
|
|
175
|
+
expect(result[0]).toBe(action)
|
|
176
|
+
expect(warnSpy).not.toHaveBeenCalled()
|
|
177
|
+
})
|
|
178
|
+
})
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import {DocumentId, getPublishedId} from '@sanity/id-utils'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type DocumentResource,
|
|
5
|
+
isCanvasResource,
|
|
6
|
+
isMediaLibraryResource,
|
|
7
|
+
} from '../config/sanityConfig'
|
|
8
|
+
import {isReleasePerspective} from '../releases/utils/isReleasePerspective'
|
|
9
|
+
import {type Action, type DocumentAction} from './actions'
|
|
10
|
+
import {getEffectiveDocumentId} from './util'
|
|
11
|
+
|
|
12
|
+
export interface EffectiveDocModel {
|
|
13
|
+
/**
|
|
14
|
+
* If this is `undefined`, the resource has no opinion and the handle's `liveEdit`
|
|
15
|
+
* flag should be respected.
|
|
16
|
+
*/
|
|
17
|
+
liveEdit: boolean | undefined
|
|
18
|
+
/**
|
|
19
|
+
* When `false`, the resource does not support release perspectives. Callers
|
|
20
|
+
* should drop any release perspective and fall back to the standard path.
|
|
21
|
+
*/
|
|
22
|
+
supportsReleases: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const MEDIA_LIBRARY_DRAFTED_TYPES = new Set(['sanity.asset'])
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Different resources have different "default" editing models.
|
|
29
|
+
*
|
|
30
|
+
* Canvas uses a liveEdit model.
|
|
31
|
+
* Medial Library is mostly liveEdit except for `sanity.asset`, which retains the
|
|
32
|
+
* draft/published model. Neither resource supports release perspectives.
|
|
33
|
+
*/
|
|
34
|
+
export function getEffectiveDocumentModel(
|
|
35
|
+
resource: DocumentResource | undefined,
|
|
36
|
+
documentType: string | undefined,
|
|
37
|
+
): EffectiveDocModel {
|
|
38
|
+
if (!resource) {
|
|
39
|
+
return {liveEdit: undefined, supportsReleases: true}
|
|
40
|
+
}
|
|
41
|
+
if (isCanvasResource(resource)) {
|
|
42
|
+
return {liveEdit: true, supportsReleases: false}
|
|
43
|
+
}
|
|
44
|
+
if (isMediaLibraryResource(resource)) {
|
|
45
|
+
const isDrafted = documentType ? MEDIA_LIBRARY_DRAFTED_TYPES.has(documentType) : false
|
|
46
|
+
return {liveEdit: !isDrafted, supportsReleases: false}
|
|
47
|
+
}
|
|
48
|
+
return {liveEdit: undefined, supportsReleases: true}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function describeResource(resource: DocumentResource | undefined): string {
|
|
52
|
+
if (resource && isCanvasResource(resource)) return 'Canvas'
|
|
53
|
+
if (resource && isMediaLibraryResource(resource)) return 'Media Library'
|
|
54
|
+
return 'this resource'
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Rewrites edit actions to match the editing model of the bound resource.
|
|
59
|
+
*
|
|
60
|
+
* Canvas and Media Library resources do not support release perspectives, and
|
|
61
|
+
* Canvas (plus most Media Library types) does not have a draft/published
|
|
62
|
+
* model. When an edit action arrives with a release perspective for a resource
|
|
63
|
+
* that doesn't support it, the perspective is stripped and a single warning
|
|
64
|
+
* is logged. When the resource forces liveEdit, the action's `liveEdit` flag
|
|
65
|
+
* is set so the dispatcher takes the liveEdit branch.
|
|
66
|
+
*
|
|
67
|
+
* Only `document.edit` actions are normalized today — other action types
|
|
68
|
+
* (publish, unpublish, release actions, etc.) pass through unchanged.
|
|
69
|
+
*
|
|
70
|
+
* @internal
|
|
71
|
+
*/
|
|
72
|
+
export function normalizeActionsForResource(
|
|
73
|
+
actions: Action[],
|
|
74
|
+
resource: DocumentResource | undefined,
|
|
75
|
+
): Action[] {
|
|
76
|
+
// collect actions that may have changed in unexpected ways
|
|
77
|
+
const stripped: Array<{documentType: string; documentId: string}> = []
|
|
78
|
+
|
|
79
|
+
const normalized = actions.map((action) => {
|
|
80
|
+
if (action.type !== 'document.edit') return action
|
|
81
|
+
|
|
82
|
+
const {liveEdit: forcedLiveEdit, supportsReleases} = getEffectiveDocumentModel(
|
|
83
|
+
resource,
|
|
84
|
+
action.documentType,
|
|
85
|
+
)
|
|
86
|
+
const shouldRemovePerspective = isReleasePerspective(action.perspective) && !supportsReleases
|
|
87
|
+
const shouldForceLiveEdit = forcedLiveEdit === true && !action.liveEdit
|
|
88
|
+
|
|
89
|
+
if (!shouldRemovePerspective && !shouldForceLiveEdit) return action
|
|
90
|
+
|
|
91
|
+
const corrected: DocumentAction = {...action}
|
|
92
|
+
if (shouldForceLiveEdit) corrected.liveEdit = true
|
|
93
|
+
if (shouldRemovePerspective) corrected.perspective = undefined
|
|
94
|
+
|
|
95
|
+
// ensure we're using the right document ID for the corrected model
|
|
96
|
+
corrected.documentId = getEffectiveDocumentId({
|
|
97
|
+
...corrected,
|
|
98
|
+
documentId: getPublishedId(DocumentId(corrected.documentId)),
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
if (shouldRemovePerspective) {
|
|
102
|
+
stripped.push({documentType: action.documentType, documentId: corrected.documentId})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return corrected
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
if (stripped.length > 0) {
|
|
109
|
+
const docs = stripped.map((e) => `${e.documentType} (${e.documentId})`).join(', ')
|
|
110
|
+
// eslint-disable-next-line no-console
|
|
111
|
+
console.warn(
|
|
112
|
+
`[sanity-sdk] ${describeResource(resource)} does not support release perspectives — falling back to the standard editing path for: ${docs}`,
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return normalized
|
|
117
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {type ReleaseDocument} from '@sanity/client'
|
|
1
2
|
import {filter, firstValueFrom, of, Subject, take} from 'rxjs'
|
|
2
3
|
import {describe, expect, it, vi} from 'vitest'
|
|
3
4
|
|
|
@@ -6,7 +7,6 @@ import {getQueryState} from '../query/queryStore'
|
|
|
6
7
|
import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
|
|
7
8
|
import {type StateSource} from '../store/createStateSourceAction'
|
|
8
9
|
import {getPerspectiveState} from './getPerspectiveState'
|
|
9
|
-
import {type ReleaseDocument} from './releasesStore'
|
|
10
10
|
|
|
11
11
|
vi.mock('../query/queryStore')
|
|
12
12
|
|
|
@@ -1,10 +1,11 @@
|
|
|
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
5
|
import {getQueryState, resolveQuery} from '../query/queryStore'
|
|
5
6
|
import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
|
|
6
7
|
import {type StateSource} from '../store/createStateSourceAction'
|
|
7
|
-
import {getActiveReleasesState,
|
|
8
|
+
import {getActiveReleasesState, getAllReleasesState} from './releasesStore'
|
|
8
9
|
|
|
9
10
|
// Mock dependencies
|
|
10
11
|
vi.mock('../query/queryStore')
|
|
@@ -167,6 +168,54 @@ describe('releasesStore', () => {
|
|
|
167
168
|
expect(state.getCurrent()).toEqual([])
|
|
168
169
|
})
|
|
169
170
|
|
|
171
|
+
it('exposes archived/published releases through getAllReleasesState but filters them out of getActiveReleasesState', async () => {
|
|
172
|
+
const subject = new Subject<ReleaseDocument[]>()
|
|
173
|
+
vi.mocked(getQueryState).mockReturnValue({
|
|
174
|
+
subscribe: () => () => {},
|
|
175
|
+
getCurrent: () => undefined,
|
|
176
|
+
observable: subject.asObservable(),
|
|
177
|
+
} as StateSource<ReleaseDocument[] | undefined>)
|
|
178
|
+
|
|
179
|
+
const active = getActiveReleasesState(instance, {
|
|
180
|
+
resource: {projectId: 'test', dataset: 'test'},
|
|
181
|
+
})
|
|
182
|
+
const all = getAllReleasesState(instance, {resource: {projectId: 'test', dataset: 'test'}})
|
|
183
|
+
|
|
184
|
+
const releases: ReleaseDocument[] = [
|
|
185
|
+
{
|
|
186
|
+
_id: 'r-active',
|
|
187
|
+
_type: 'system.release',
|
|
188
|
+
name: 'r-active',
|
|
189
|
+
state: 'active',
|
|
190
|
+
metadata: {releaseType: 'asap'},
|
|
191
|
+
} as ReleaseDocument,
|
|
192
|
+
{
|
|
193
|
+
_id: 'r-archived',
|
|
194
|
+
_type: 'system.release',
|
|
195
|
+
name: 'r-archived',
|
|
196
|
+
state: 'archived',
|
|
197
|
+
metadata: {releaseType: 'asap'},
|
|
198
|
+
} as ReleaseDocument,
|
|
199
|
+
{
|
|
200
|
+
_id: 'r-published',
|
|
201
|
+
_type: 'system.release',
|
|
202
|
+
name: 'r-published',
|
|
203
|
+
state: 'published',
|
|
204
|
+
metadata: {releaseType: 'asap'},
|
|
205
|
+
} as ReleaseDocument,
|
|
206
|
+
]
|
|
207
|
+
|
|
208
|
+
subject.next(releases)
|
|
209
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
210
|
+
|
|
211
|
+
const activeNames = active.getCurrent()?.map((r) => r.name) ?? []
|
|
212
|
+
const allNames = all.getCurrent()?.map((r) => r.name) ?? []
|
|
213
|
+
|
|
214
|
+
expect(activeNames).toEqual(['r-active'])
|
|
215
|
+
expect(allNames).toEqual(expect.arrayContaining(['r-active', 'r-archived', 'r-published']))
|
|
216
|
+
expect(allNames).toHaveLength(3)
|
|
217
|
+
})
|
|
218
|
+
|
|
170
219
|
it('should not crash when the releases query errors', async () => {
|
|
171
220
|
const subject = new Subject<ReleaseDocument[]>()
|
|
172
221
|
vi.mocked(getQueryState).mockReturnValue({
|
|
@@ -1,4 +1,4 @@
|
|
|
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'
|
|
@@ -21,23 +21,23 @@ const ARCHIVED_RELEASE_STATES = ['archived', 'published']
|
|
|
21
21
|
const STABLE_EMPTY_RELEASES: ReleaseDocument[] = []
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
24
|
+
* Lifecycle states a release document can be in. Mirrors the server's
|
|
25
|
+
* `ReleaseState`.
|
|
26
|
+
* @beta
|
|
26
27
|
*/
|
|
27
|
-
export type
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
}
|
|
28
|
+
export type ReleaseState =
|
|
29
|
+
| 'active'
|
|
30
|
+
| 'archiving'
|
|
31
|
+
| 'unarchiving'
|
|
32
|
+
| 'archived'
|
|
33
|
+
| 'published'
|
|
34
|
+
| 'publishing'
|
|
35
|
+
| 'scheduled'
|
|
36
|
+
| 'scheduling'
|
|
38
37
|
|
|
39
38
|
export interface ReleasesStoreState {
|
|
40
39
|
activeReleases?: ReleaseDocument[]
|
|
40
|
+
allReleases?: ReleaseDocument[]
|
|
41
41
|
error?: unknown
|
|
42
42
|
}
|
|
43
43
|
|
|
@@ -45,6 +45,7 @@ export const releasesStore = defineStore<ReleasesStoreState, BoundResourceKey>({
|
|
|
45
45
|
name: 'Releases',
|
|
46
46
|
getInitialState: (): ReleasesStoreState => ({
|
|
47
47
|
activeReleases: undefined,
|
|
48
|
+
allReleases: undefined,
|
|
48
49
|
}),
|
|
49
50
|
initialize: (context) => {
|
|
50
51
|
const subscription = subscribeToReleases(context)
|
|
@@ -74,6 +75,26 @@ export const getActiveReleasesState = (
|
|
|
74
75
|
// bindActionByResource keyFn destructures { resource } from the first param, so pass {} when no options
|
|
75
76
|
_getActiveReleasesState(instance, options ?? {})
|
|
76
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Get every release in the store, including archived and published.
|
|
80
|
+
* @internal
|
|
81
|
+
*/
|
|
82
|
+
const _getAllReleasesState = bindActionByResource(
|
|
83
|
+
releasesStore,
|
|
84
|
+
createStateSourceAction({
|
|
85
|
+
selector: ({state}, _?) => state.allReleases,
|
|
86
|
+
}),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get every release in the store, including archived and published.
|
|
91
|
+
* @internal
|
|
92
|
+
*/
|
|
93
|
+
export const getAllReleasesState = (
|
|
94
|
+
instance: SanityInstance,
|
|
95
|
+
options?: {resource?: DocumentResource},
|
|
96
|
+
): StateSource<ReleaseDocument[] | undefined> => _getAllReleasesState(instance, options ?? {})
|
|
97
|
+
|
|
77
98
|
const RELEASES_QUERY = 'releases::all()'
|
|
78
99
|
|
|
79
100
|
const subscribeToReleases = ({
|
|
@@ -92,10 +113,12 @@ const subscribeToReleases = ({
|
|
|
92
113
|
map((releases) => {
|
|
93
114
|
// logic here mirrors that of studio:
|
|
94
115
|
// https://github.com/sanity-io/sanity/blob/156e8fa482703d99219f08da7bacb384517f1513/packages/sanity/src/core/releases/store/useActiveReleases.ts#L29
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
116
|
+
const sorted = sortReleases(releases ?? STABLE_EMPTY_RELEASES).reverse()
|
|
117
|
+
state.set('setReleases', {
|
|
118
|
+
allReleases: sorted,
|
|
119
|
+
activeReleases: sorted.filter(
|
|
120
|
+
(release) => !ARCHIVED_RELEASE_STATES.includes(release.state),
|
|
121
|
+
),
|
|
99
122
|
})
|
|
100
123
|
}),
|
|
101
124
|
)
|
|
@@ -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',
|
|
@@ -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
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import {afterEach, describe, expect, it, vi} from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {getTelemetryEnvironment, isTelemetryEnabled} from './environment'
|
|
4
|
+
|
|
5
|
+
describe('getTelemetryEnvironment', () => {
|
|
6
|
+
afterEach(() => {
|
|
7
|
+
vi.unstubAllEnvs()
|
|
8
|
+
vi.unstubAllGlobals()
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
describe('browser', () => {
|
|
12
|
+
it('returns "development" on localhost', () => {
|
|
13
|
+
vi.stubGlobal('window', {location: {hostname: 'localhost'}})
|
|
14
|
+
expect(getTelemetryEnvironment()).toBe('development')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('returns "development" on 127.0.0.1', () => {
|
|
18
|
+
vi.stubGlobal('window', {location: {hostname: '127.0.0.1'}})
|
|
19
|
+
expect(getTelemetryEnvironment()).toBe('development')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('returns "production" on *.sanity.studio', () => {
|
|
23
|
+
vi.stubGlobal('window', {location: {hostname: 'myapp.sanity.studio'}})
|
|
24
|
+
expect(getTelemetryEnvironment()).toBe('production')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('returns "production" on www.sanity.io (dashboard)', () => {
|
|
28
|
+
vi.stubGlobal('window', {location: {hostname: 'www.sanity.io'}})
|
|
29
|
+
expect(getTelemetryEnvironment()).toBe('production')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('returns null on *.sanity.work (staging is intentionally not allowlisted)', () => {
|
|
33
|
+
vi.stubGlobal('window', {location: {hostname: 'www.sanity.work'}})
|
|
34
|
+
expect(getTelemetryEnvironment()).toBeNull()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('returns null on *.sanity.dev (preview hosts are intentionally not allowlisted)', () => {
|
|
38
|
+
vi.stubGlobal('window', {location: {hostname: 'preview-123.sanity.dev'}})
|
|
39
|
+
expect(getTelemetryEnvironment()).toBeNull()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('is case-insensitive on hostname', () => {
|
|
43
|
+
vi.stubGlobal('window', {location: {hostname: 'MyApp.Sanity.Studio'}})
|
|
44
|
+
expect(getTelemetryEnvironment()).toBe('production')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('returns null on a customer-controlled domain', () => {
|
|
48
|
+
vi.stubGlobal('window', {location: {hostname: 'myapp.customer.com'}})
|
|
49
|
+
expect(getTelemetryEnvironment()).toBeNull()
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('returns null on a lookalike subdomain (suffix match requires a leading dot)', () => {
|
|
53
|
+
// `evilsanity.studio` ends in `sanity.studio` but not `.sanity.studio`,
|
|
54
|
+
// so the suffix check rejects it.
|
|
55
|
+
vi.stubGlobal('window', {location: {hostname: 'evilsanity.studio'}})
|
|
56
|
+
expect(getTelemetryEnvironment()).toBeNull()
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('returns null on the bare apex hostname (sanity.studio with no subdomain)', () => {
|
|
60
|
+
// The allowlist intentionally only matches subdomains (the leading
|
|
61
|
+
// `.` in `.sanity.studio` means the apex `sanity.studio` is excluded).
|
|
62
|
+
vi.stubGlobal('window', {location: {hostname: 'sanity.studio'}})
|
|
63
|
+
expect(getTelemetryEnvironment()).toBeNull()
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('returns null when hostname is missing', () => {
|
|
67
|
+
vi.stubGlobal('window', {location: {}})
|
|
68
|
+
expect(getTelemetryEnvironment()).toBeNull()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('returns "development" on localhost even when NODE_ENV is production', () => {
|
|
72
|
+
vi.stubEnv('NODE_ENV', 'production')
|
|
73
|
+
vi.stubGlobal('window', {location: {hostname: 'localhost'}})
|
|
74
|
+
expect(getTelemetryEnvironment()).toBe('development')
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
describe('node', () => {
|
|
79
|
+
it('returns "development" when NODE_ENV=development and no window', () => {
|
|
80
|
+
vi.stubEnv('NODE_ENV', 'development')
|
|
81
|
+
vi.stubGlobal('window', undefined)
|
|
82
|
+
expect(getTelemetryEnvironment()).toBe('development')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('returns null when NODE_ENV=production and no window', () => {
|
|
86
|
+
vi.stubEnv('NODE_ENV', 'production')
|
|
87
|
+
vi.stubGlobal('window', undefined)
|
|
88
|
+
expect(getTelemetryEnvironment()).toBeNull()
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('returns null when NODE_ENV=test and no window', () => {
|
|
92
|
+
vi.stubEnv('NODE_ENV', 'test')
|
|
93
|
+
vi.stubGlobal('window', undefined)
|
|
94
|
+
expect(getTelemetryEnvironment()).toBeNull()
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
describe('isTelemetryEnabled', () => {
|
|
100
|
+
afterEach(() => {
|
|
101
|
+
vi.unstubAllEnvs()
|
|
102
|
+
vi.unstubAllGlobals()
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('returns true on a Sanity-controlled domain', () => {
|
|
106
|
+
vi.stubGlobal('window', {location: {hostname: 'app.sanity.studio'}})
|
|
107
|
+
expect(isTelemetryEnabled()).toBe(true)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('returns true on localhost', () => {
|
|
111
|
+
vi.stubGlobal('window', {location: {hostname: 'localhost'}})
|
|
112
|
+
expect(isTelemetryEnabled()).toBe(true)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('returns false on a customer domain', () => {
|
|
116
|
+
vi.stubGlobal('window', {location: {hostname: 'myapp.example.com'}})
|
|
117
|
+
expect(isTelemetryEnabled()).toBe(false)
|
|
118
|
+
})
|
|
119
|
+
})
|