@sanity/sdk 2.7.0 → 2.9.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 +2396 -0
- package/dist/_chunks-es/_internal.js +129 -0
- package/dist/_chunks-es/_internal.js.map +1 -0
- package/dist/_chunks-es/createGroqSearchFilter.js +1460 -0
- package/dist/_chunks-es/createGroqSearchFilter.js.map +1 -0
- package/dist/_chunks-es/telemetryManager.js +87 -0
- package/dist/_chunks-es/telemetryManager.js.map +1 -0
- package/dist/_chunks-es/version.js +7 -0
- package/dist/_chunks-es/version.js.map +1 -0
- package/dist/_exports/_internal.d.ts +64 -0
- package/dist/_exports/_internal.js +20 -0
- package/dist/_exports/_internal.js.map +1 -0
- package/dist/index.d.ts +2 -2343
- package/dist/index.js +383 -1777
- package/dist/index.js.map +1 -1
- package/package.json +11 -4
- package/src/_exports/_internal.ts +14 -0
- package/src/_exports/index.ts +10 -1
- package/src/auth/authStore.test.ts +150 -1
- package/src/auth/authStore.ts +11 -11
- package/src/auth/dashboardAuth.ts +2 -2
- package/src/auth/handleAuthCallback.ts +9 -3
- package/src/auth/logout.test.ts +1 -1
- package/src/auth/logout.ts +1 -1
- package/src/auth/refreshStampedToken.test.ts +118 -1
- package/src/auth/refreshStampedToken.ts +3 -2
- package/src/auth/standaloneAuth.ts +9 -3
- package/src/auth/studioAuth.ts +34 -7
- package/src/auth/studioModeAuth.ts +2 -1
- package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +10 -2
- package/src/auth/subscribeToStateAndFetchCurrentUser.ts +5 -1
- package/src/auth/subscribeToStorageEventsAndSetToken.ts +2 -2
- package/src/auth/utils.ts +33 -0
- package/src/client/clientStore.test.ts +14 -0
- package/src/client/clientStore.ts +2 -1
- package/src/comlink/node/getNodeState.ts +2 -1
- package/src/config/sanityConfig.ts +6 -0
- package/src/document/actions.ts +18 -11
- package/src/document/applyDocumentActions.test.ts +7 -6
- package/src/document/applyDocumentActions.ts +10 -4
- package/src/document/documentStore.test.ts +536 -188
- package/src/document/documentStore.ts +142 -76
- package/src/document/events.ts +7 -2
- package/src/document/permissions.test.ts +18 -16
- package/src/document/permissions.ts +35 -11
- package/src/document/processActions.test.ts +359 -32
- package/src/document/processActions.ts +104 -76
- package/src/document/reducers.test.ts +117 -29
- package/src/document/reducers.ts +43 -36
- package/src/document/sharedListener.ts +16 -6
- package/src/document/util.ts +14 -0
- package/src/favorites/favorites.test.ts +9 -2
- package/src/presence/bifurTransport.ts +6 -1
- package/src/preview/getPreviewState.test.ts +115 -98
- package/src/preview/getPreviewState.ts +38 -60
- package/src/preview/previewProjectionUtils.test.ts +179 -0
- package/src/preview/previewProjectionUtils.ts +93 -0
- package/src/preview/resolvePreview.test.ts +42 -25
- package/src/preview/resolvePreview.ts +29 -10
- package/src/preview/{previewStore.ts → types.ts} +8 -17
- package/src/projection/getProjectionState.test.ts +16 -16
- package/src/projection/getProjectionState.ts +2 -1
- package/src/projection/projectionQuery.ts +2 -3
- package/src/projection/types.ts +1 -1
- package/src/query/queryStore.ts +2 -1
- package/src/releases/getPerspectiveState.ts +7 -6
- package/src/releases/releasesStore.test.ts +20 -5
- package/src/releases/releasesStore.ts +20 -8
- package/src/store/createStateSourceAction.test.ts +62 -0
- package/src/store/createStateSourceAction.ts +34 -39
- package/src/telemetry/__telemetry__/sdk.telemetry.ts +42 -0
- package/src/telemetry/devMode.test.ts +52 -0
- package/src/telemetry/devMode.ts +40 -0
- package/src/telemetry/initTelemetry.test.ts +225 -0
- package/src/telemetry/initTelemetry.ts +205 -0
- package/src/telemetry/telemetryManager.test.ts +263 -0
- package/src/telemetry/telemetryManager.ts +187 -0
- package/src/users/usersStore.test.ts +1 -0
- package/src/users/usersStore.ts +5 -1
- package/src/utils/createFetcherStore.test.ts +6 -4
- package/src/utils/createFetcherStore.ts +2 -1
- package/src/utils/getStagingApiHost.test.ts +21 -0
- package/src/utils/getStagingApiHost.ts +14 -0
- package/src/utils/ids.test.ts +1 -29
- package/src/utils/ids.ts +0 -10
- package/src/utils/setCleanupTimeout.ts +24 -0
- package/src/preview/previewQuery.test.ts +0 -236
- package/src/preview/previewQuery.ts +0 -153
- package/src/preview/previewStore.test.ts +0 -36
- package/src/preview/subscribeToStateAndFetchBatches.test.ts +0 -221
- package/src/preview/subscribeToStateAndFetchBatches.ts +0 -112
- package/src/preview/util.ts +0 -13
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import {type SanityClient} from '@sanity/client'
|
|
2
|
+
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
3
|
+
|
|
4
|
+
import {createSanityInstance} from '../store/createSanityInstance'
|
|
5
|
+
import {normalizeMedia, transformProjectionToPreview} from './previewProjectionUtils'
|
|
6
|
+
import {type PreviewQueryResult} from './types'
|
|
7
|
+
|
|
8
|
+
// Mock the getClient function
|
|
9
|
+
vi.mock('../client/clientStore', () => ({
|
|
10
|
+
getClient: vi.fn(),
|
|
11
|
+
}))
|
|
12
|
+
|
|
13
|
+
const mockClient = {
|
|
14
|
+
config: () => ({projectId: 'test-project', dataset: 'test-dataset'}),
|
|
15
|
+
} as unknown as SanityClient
|
|
16
|
+
|
|
17
|
+
describe('normalizeMedia', () => {
|
|
18
|
+
beforeEach(async () => {
|
|
19
|
+
vi.clearAllMocks()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('returns null if media is null or undefined', () => {
|
|
23
|
+
expect(normalizeMedia(null, mockClient)).toBeNull()
|
|
24
|
+
expect(normalizeMedia(undefined, mockClient)).toBeNull()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('returns null if media does not have a valid asset', () => {
|
|
28
|
+
const invalidMedia1 = {media: {_ref: 'image-abc123-200x200-png'}} // Missing `asset` property
|
|
29
|
+
const invalidMedia2 = {asset: {ref: 'image-abc123-200x200-png'}} // Incorrect property name `ref`
|
|
30
|
+
expect(normalizeMedia(invalidMedia1, mockClient)).toBeNull()
|
|
31
|
+
expect(normalizeMedia(invalidMedia2, mockClient)).toBeNull()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('returns null if media is not an object', () => {
|
|
35
|
+
expect(normalizeMedia(123, mockClient)).toBeNull()
|
|
36
|
+
expect(normalizeMedia('invalid', mockClient)).toBeNull()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('returns a normalized URL for valid image asset objects', () => {
|
|
40
|
+
const validMedia = {type: 'image-asset', _ref: 'image-abc123-200x200-png'}
|
|
41
|
+
const result = normalizeMedia(validMedia, mockClient)
|
|
42
|
+
expect(result).toEqual({
|
|
43
|
+
type: 'image-asset',
|
|
44
|
+
_ref: 'image-abc123-200x200-png',
|
|
45
|
+
url: 'https://cdn.sanity.io/images/test-project/test-dataset/abc123-200x200.png',
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('handles image assets with expected URL format', () => {
|
|
50
|
+
const media = {type: 'image-asset', _ref: 'image-xyz456-400x400-jpg'}
|
|
51
|
+
const result = normalizeMedia(media, mockClient)
|
|
52
|
+
expect(result).toEqual({
|
|
53
|
+
type: 'image-asset',
|
|
54
|
+
_ref: 'image-xyz456-400x400-jpg',
|
|
55
|
+
url: 'https://cdn.sanity.io/images/test-project/test-dataset/xyz456-400x400.jpg',
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe('transformProjectionToPreview', () => {
|
|
61
|
+
const instance = createSanityInstance({
|
|
62
|
+
projectId: 'test-project',
|
|
63
|
+
dataset: 'test-dataset',
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
beforeEach(async () => {
|
|
67
|
+
vi.clearAllMocks()
|
|
68
|
+
|
|
69
|
+
// Mock getClient to return our mock client
|
|
70
|
+
const {getClient} = await import('../client/clientStore')
|
|
71
|
+
vi.mocked(getClient).mockReturnValue(mockClient)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('transforms projection result with title and subtitle', () => {
|
|
75
|
+
const projectionResult: PreviewQueryResult = {
|
|
76
|
+
_id: 'doc1',
|
|
77
|
+
_type: 'article',
|
|
78
|
+
_updatedAt: '2026-01-01',
|
|
79
|
+
titleCandidates: {title: 'My Title'},
|
|
80
|
+
subtitleCandidates: {description: 'My Description'},
|
|
81
|
+
media: null,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const result = transformProjectionToPreview(instance, projectionResult)
|
|
85
|
+
|
|
86
|
+
expect(result).toEqual({
|
|
87
|
+
title: 'My Title',
|
|
88
|
+
subtitle: 'My Description',
|
|
89
|
+
media: null,
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('uses fallback title when no title candidates exist', () => {
|
|
94
|
+
const projectionResult: PreviewQueryResult = {
|
|
95
|
+
_id: 'doc1',
|
|
96
|
+
_type: 'article',
|
|
97
|
+
_updatedAt: '2026-01-01',
|
|
98
|
+
titleCandidates: {},
|
|
99
|
+
subtitleCandidates: {},
|
|
100
|
+
media: null,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const result = transformProjectionToPreview(instance, projectionResult)
|
|
104
|
+
|
|
105
|
+
expect(result.title).toBe('article: doc1')
|
|
106
|
+
expect(result.subtitle).toBeUndefined()
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('transforms projection result with media', () => {
|
|
110
|
+
const projectionResult: PreviewQueryResult = {
|
|
111
|
+
_id: 'doc1',
|
|
112
|
+
_type: 'article',
|
|
113
|
+
_updatedAt: '2026-01-01',
|
|
114
|
+
titleCandidates: {title: 'My Title'},
|
|
115
|
+
subtitleCandidates: {},
|
|
116
|
+
media: {type: 'image-asset', _ref: 'image-abc123-200x200-png', url: ''},
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const result = transformProjectionToPreview(instance, projectionResult)
|
|
120
|
+
|
|
121
|
+
expect(result).toEqual({
|
|
122
|
+
title: 'My Title',
|
|
123
|
+
subtitle: undefined,
|
|
124
|
+
media: {
|
|
125
|
+
type: 'image-asset',
|
|
126
|
+
_ref: 'image-abc123-200x200-png',
|
|
127
|
+
url: 'https://cdn.sanity.io/images/test-project/test-dataset/abc123-200x200.png',
|
|
128
|
+
},
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('includes status when provided', () => {
|
|
133
|
+
const projectionResult: PreviewQueryResult = {
|
|
134
|
+
_id: 'doc1',
|
|
135
|
+
_type: 'article',
|
|
136
|
+
_updatedAt: '2026-01-01',
|
|
137
|
+
titleCandidates: {title: 'My Title'},
|
|
138
|
+
subtitleCandidates: {},
|
|
139
|
+
media: null,
|
|
140
|
+
_status: {
|
|
141
|
+
lastEditedPublishedAt: '2026-01-01',
|
|
142
|
+
lastEditedDraftAt: '2026-01-02',
|
|
143
|
+
},
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const result = transformProjectionToPreview(instance, projectionResult)
|
|
147
|
+
|
|
148
|
+
expect(result).toEqual({
|
|
149
|
+
title: 'My Title',
|
|
150
|
+
subtitle: undefined,
|
|
151
|
+
media: null,
|
|
152
|
+
_status: {
|
|
153
|
+
lastEditedPublishedAt: '2026-01-01',
|
|
154
|
+
lastEditedDraftAt: '2026-01-02',
|
|
155
|
+
},
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('calls getClient with the provided source', async () => {
|
|
160
|
+
const projectionResult: PreviewQueryResult = {
|
|
161
|
+
_id: 'doc1',
|
|
162
|
+
_type: 'article',
|
|
163
|
+
_updatedAt: '2026-01-01',
|
|
164
|
+
titleCandidates: {title: 'My Title'},
|
|
165
|
+
subtitleCandidates: {},
|
|
166
|
+
media: null,
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const source = {mediaLibraryId: 'test-library'}
|
|
170
|
+
|
|
171
|
+
transformProjectionToPreview(instance, projectionResult, source)
|
|
172
|
+
|
|
173
|
+
const {getClient} = await import('../client/clientStore')
|
|
174
|
+
expect(getClient).toHaveBeenCalledWith(instance, {
|
|
175
|
+
apiVersion: 'v2025-05-06',
|
|
176
|
+
source,
|
|
177
|
+
})
|
|
178
|
+
})
|
|
179
|
+
})
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import {type SanityClient} from '@sanity/client'
|
|
2
|
+
import {createImageUrlBuilder} from '@sanity/image-url'
|
|
3
|
+
import {isObject} from 'lodash-es'
|
|
4
|
+
|
|
5
|
+
import {getClient} from '../client/clientStore'
|
|
6
|
+
import {type DocumentSource, isDatasetSource} from '../config/sanityConfig'
|
|
7
|
+
import {type SanityInstance} from '../store/createSanityInstance'
|
|
8
|
+
import {SUBTITLE_CANDIDATES, TITLE_CANDIDATES} from './previewConstants'
|
|
9
|
+
import {type PreviewQueryResult, type PreviewValue} from './types'
|
|
10
|
+
|
|
11
|
+
const API_VERSION = 'v2025-05-06'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Checks if the provided value has `_ref` property that is a string and starts with `image-`
|
|
15
|
+
*/
|
|
16
|
+
function hasImageRef<T>(value: unknown): value is T & {_ref: string} {
|
|
17
|
+
return isObject(value) && '_ref' in value && typeof (value as {_ref: unknown})._ref === 'string'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Normalizes a media asset to a preview value.
|
|
22
|
+
* Adds a url to a media asset reference using `@sanity/image-url`.
|
|
23
|
+
*
|
|
24
|
+
* @internal
|
|
25
|
+
*/
|
|
26
|
+
export function normalizeMedia(media: unknown, client: SanityClient): PreviewValue['media'] {
|
|
27
|
+
if (!media) return null
|
|
28
|
+
if (!hasImageRef(media)) return null
|
|
29
|
+
|
|
30
|
+
const builder = createImageUrlBuilder(client)
|
|
31
|
+
const url = builder.image({_ref: media._ref}).url()
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
type: 'image-asset',
|
|
35
|
+
_ref: media._ref,
|
|
36
|
+
url,
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Finds a single field value from a set of candidates based on a priority list of field names.
|
|
42
|
+
* Returns the first non-empty string value found from the candidates matching the priority list order.
|
|
43
|
+
*
|
|
44
|
+
* @internal
|
|
45
|
+
*/
|
|
46
|
+
function findFirstDefined(
|
|
47
|
+
fieldsToSearch: string[],
|
|
48
|
+
candidates: Record<string, unknown>,
|
|
49
|
+
exclude?: unknown,
|
|
50
|
+
): string | undefined {
|
|
51
|
+
if (!candidates) return undefined
|
|
52
|
+
|
|
53
|
+
for (const field of fieldsToSearch) {
|
|
54
|
+
const value = candidates[field]
|
|
55
|
+
if (typeof value === 'string' && value.trim() !== '' && value !== exclude) {
|
|
56
|
+
return value
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return undefined
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Transforms a projection result (with titleCandidates, subtitleCandidates, media)
|
|
65
|
+
* into a PreviewValue (with title, subtitle, media).
|
|
66
|
+
*
|
|
67
|
+
* @param projectionResult - The raw projection result from GROQ
|
|
68
|
+
* @param instance - The Sanity instance to use for client configuration
|
|
69
|
+
* @param source - Data source for the preview
|
|
70
|
+
* @internal
|
|
71
|
+
*/
|
|
72
|
+
export function transformProjectionToPreview(
|
|
73
|
+
instance: SanityInstance,
|
|
74
|
+
projectionResult: PreviewQueryResult,
|
|
75
|
+
source?: DocumentSource,
|
|
76
|
+
): PreviewValue {
|
|
77
|
+
const title = findFirstDefined(TITLE_CANDIDATES, projectionResult.titleCandidates)
|
|
78
|
+
const subtitle = findFirstDefined(SUBTITLE_CANDIDATES, projectionResult.subtitleCandidates, title)
|
|
79
|
+
|
|
80
|
+
// Get a client for the source (if provided) or use the instance config
|
|
81
|
+
const client = getClient(instance, {
|
|
82
|
+
apiVersion: API_VERSION,
|
|
83
|
+
// TODO: remove in v3 when we're ready for everything to be queried via source
|
|
84
|
+
source: source && !isDatasetSource(source) ? source : undefined,
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
title: String(title || `${projectionResult._type}: ${projectionResult._id}`),
|
|
89
|
+
subtitle: subtitle || undefined,
|
|
90
|
+
media: normalizeMedia(projectionResult.media, client),
|
|
91
|
+
...(projectionResult._status && {_status: projectionResult._status}),
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -1,28 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
|
|
1
|
+
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
3
2
|
|
|
4
|
-
import {
|
|
3
|
+
import {resolveProjection} from '../projection/resolveProjection'
|
|
4
|
+
import {type ProjectionValuePending} from '../projection/types'
|
|
5
5
|
import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
|
|
6
|
-
import {type StateSource} from '../store/createStateSourceAction'
|
|
7
|
-
import {getPreviewState} from './getPreviewState'
|
|
8
|
-
import {type PreviewValue, type ValuePending} from './previewStore'
|
|
9
6
|
import {resolvePreview} from './resolvePreview'
|
|
7
|
+
import {type PreviewQueryResult} from './types'
|
|
10
8
|
|
|
11
|
-
vi.mock('
|
|
9
|
+
vi.mock('../projection/resolveProjection')
|
|
12
10
|
|
|
13
11
|
describe('resolvePreview', () => {
|
|
14
12
|
let instance: SanityInstance
|
|
15
|
-
|
|
16
13
|
beforeEach(() => {
|
|
17
|
-
vi.
|
|
18
|
-
// Create a mock that returns the correct ValuePending type
|
|
19
|
-
vi.mocked(getPreviewState).mockReturnValue({
|
|
20
|
-
observable: of({
|
|
21
|
-
data: {title: 'test'},
|
|
22
|
-
isPending: false,
|
|
23
|
-
} as ValuePending<PreviewValue>),
|
|
24
|
-
} as StateSource<ValuePending<PreviewValue>>)
|
|
25
|
-
|
|
14
|
+
vi.clearAllMocks()
|
|
26
15
|
instance = createSanityInstance({projectId: 'p', dataset: 'd'})
|
|
27
16
|
})
|
|
28
17
|
|
|
@@ -30,18 +19,46 @@ describe('resolvePreview', () => {
|
|
|
30
19
|
instance.dispose()
|
|
31
20
|
})
|
|
32
21
|
|
|
33
|
-
it('resolves
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
22
|
+
it('resolves and transforms projection result to preview format', async () => {
|
|
23
|
+
const mockProjectionResult: PreviewQueryResult = {
|
|
24
|
+
_id: 'doc1',
|
|
25
|
+
_type: 'article',
|
|
26
|
+
_updatedAt: '2024-01-01',
|
|
27
|
+
titleCandidates: {title: 'Resolved Title'},
|
|
28
|
+
subtitleCandidates: {description: 'Resolved Description'},
|
|
29
|
+
media: null,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
vi.mocked(resolveProjection).mockResolvedValue({
|
|
33
|
+
data: mockProjectionResult,
|
|
34
|
+
isPending: false,
|
|
35
|
+
} as ProjectionValuePending<PreviewQueryResult>)
|
|
36
|
+
|
|
37
|
+
const result = await resolvePreview(instance, {
|
|
38
|
+
documentId: 'doc1',
|
|
39
|
+
documentType: 'article',
|
|
37
40
|
})
|
|
38
41
|
|
|
39
|
-
|
|
42
|
+
expect(result.data).toEqual({
|
|
43
|
+
title: 'Resolved Title',
|
|
44
|
+
subtitle: 'Resolved Description',
|
|
45
|
+
media: null,
|
|
46
|
+
})
|
|
47
|
+
expect(result.isPending).toBe(false)
|
|
48
|
+
})
|
|
40
49
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
data:
|
|
50
|
+
it('returns null data when projection resolves with null', async () => {
|
|
51
|
+
vi.mocked(resolveProjection).mockResolvedValue({
|
|
52
|
+
data: null,
|
|
44
53
|
isPending: false,
|
|
45
54
|
})
|
|
55
|
+
|
|
56
|
+
const result = await resolvePreview(instance, {
|
|
57
|
+
documentId: 'doc1',
|
|
58
|
+
documentType: 'article',
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
expect(result.data).toBeNull()
|
|
62
|
+
expect(result.isPending).toBe(false)
|
|
46
63
|
})
|
|
47
64
|
})
|
|
@@ -1,20 +1,39 @@
|
|
|
1
|
-
import {filter, firstValueFrom} from 'rxjs'
|
|
2
|
-
|
|
3
1
|
import {type DocumentHandle} from '../config/sanityConfig'
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
2
|
+
import {resolveProjection} from '../projection/resolveProjection'
|
|
3
|
+
import {type SanityInstance} from '../store/createSanityInstance'
|
|
4
|
+
import {PREVIEW_PROJECTION} from './previewConstants'
|
|
5
|
+
import {transformProjectionToPreview} from './previewProjectionUtils'
|
|
6
|
+
import {type PreviewQueryResult, type PreviewValue, type ValuePending} from './types'
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* @beta
|
|
10
|
+
* @deprecated This type is deprecated and will be removed in a future release.
|
|
10
11
|
*/
|
|
11
12
|
export type ResolvePreviewOptions = DocumentHandle
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* @beta
|
|
16
|
+
* @deprecated This function is deprecated and will be removed in a future release.
|
|
15
17
|
*/
|
|
16
|
-
export
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
export async function resolvePreview(
|
|
19
|
+
instance: SanityInstance,
|
|
20
|
+
options: ResolvePreviewOptions,
|
|
21
|
+
): Promise<ValuePending<PreviewValue>> {
|
|
22
|
+
// Resolve the projection
|
|
23
|
+
const projectionResult = await resolveProjection<PreviewQueryResult>(instance, {
|
|
24
|
+
...options,
|
|
25
|
+
projection: PREVIEW_PROJECTION,
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
if (!projectionResult.data) {
|
|
29
|
+
return {data: null, isPending: projectionResult.isPending}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Transform to preview format
|
|
33
|
+
const previewValue = transformProjectionToPreview(instance, projectionResult.data, options.source)
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
data: previewValue,
|
|
37
|
+
isPending: projectionResult.isPending,
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import {type
|
|
2
|
-
import {defineStore} from '../store/defineStore'
|
|
3
|
-
import {subscribeToStateAndFetchBatches} from './subscribeToStateAndFetchBatches'
|
|
1
|
+
import {type DocumentStatus} from '../projection/types'
|
|
4
2
|
|
|
3
|
+
/**
|
|
4
|
+
*
|
|
5
|
+
* @internal
|
|
6
|
+
*/
|
|
5
7
|
export interface PreviewQueryResult {
|
|
6
8
|
_id: string
|
|
7
9
|
_type: string
|
|
@@ -9,6 +11,7 @@ export interface PreviewQueryResult {
|
|
|
9
11
|
titleCandidates: Record<string, unknown>
|
|
10
12
|
subtitleCandidates: Record<string, unknown>
|
|
11
13
|
media?: PreviewMedia | null
|
|
14
|
+
_status?: DocumentStatus
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
/**
|
|
@@ -74,22 +77,10 @@ export type ValuePending<T> = {
|
|
|
74
77
|
|
|
75
78
|
/**
|
|
76
79
|
* @public
|
|
80
|
+
* @deprecated This interface is kept for backwards compatibility but is no longer used internally.
|
|
81
|
+
* Preview state is now stored in the projection store.
|
|
77
82
|
*/
|
|
78
83
|
export interface PreviewStoreState {
|
|
79
84
|
values: {[TDocumentId in string]?: ValuePending<PreviewValue>}
|
|
80
85
|
subscriptions: {[TDocumentId in string]?: {[TSubscriptionId in string]?: true}}
|
|
81
86
|
}
|
|
82
|
-
|
|
83
|
-
export const previewStore = defineStore<PreviewStoreState, BoundDatasetKey>({
|
|
84
|
-
name: 'Preview',
|
|
85
|
-
getInitialState() {
|
|
86
|
-
return {
|
|
87
|
-
subscriptions: {},
|
|
88
|
-
values: {},
|
|
89
|
-
}
|
|
90
|
-
},
|
|
91
|
-
initialize: (context) => {
|
|
92
|
-
const subscription = subscribeToStateAndFetchBatches(context)
|
|
93
|
-
return () => subscription.unsubscribe
|
|
94
|
-
},
|
|
95
|
-
})
|
|
@@ -126,7 +126,7 @@ describe('getProjectionState', () => {
|
|
|
126
126
|
expect(state.get().subscriptions).toEqual({})
|
|
127
127
|
expect(state.get().documentProjections).toEqual({})
|
|
128
128
|
|
|
129
|
-
const unsubscribe1 = projectionState1.subscribe(vi.fn()) // Should use ID
|
|
129
|
+
const unsubscribe1 = projectionState1.subscribe(vi.fn()) // Should use ID 2
|
|
130
130
|
expect(state.get().subscriptions).toEqual({
|
|
131
131
|
[docHandle.documentId]: {[hash1]: {testSubId_2: true}},
|
|
132
132
|
})
|
|
@@ -134,7 +134,7 @@ describe('getProjectionState', () => {
|
|
|
134
134
|
[docHandle.documentId]: {[hash1]: projection1},
|
|
135
135
|
})
|
|
136
136
|
|
|
137
|
-
const unsubscribe2 = projectionState2.subscribe(vi.fn()) // Should use ID
|
|
137
|
+
const unsubscribe2 = projectionState2.subscribe(vi.fn()) // Should use ID 3
|
|
138
138
|
expect(state.get().subscriptions).toEqual({
|
|
139
139
|
[docHandle.documentId]: {
|
|
140
140
|
[hash1]: {testSubId_2: true},
|
|
@@ -148,10 +148,12 @@ describe('getProjectionState', () => {
|
|
|
148
148
|
},
|
|
149
149
|
})
|
|
150
150
|
|
|
151
|
-
|
|
151
|
+
// should share ID 2 from the first subscription -- we only care if ANY subscription exists
|
|
152
|
+
// so we can include the document / projection in a bulk fetch
|
|
153
|
+
const unsubscribe3 = projectionState1.subscribe(vi.fn())
|
|
152
154
|
expect(state.get().subscriptions).toEqual({
|
|
153
155
|
[docHandle.documentId]: {
|
|
154
|
-
[hash1]: {testSubId_2: true
|
|
156
|
+
[hash1]: {testSubId_2: true},
|
|
155
157
|
[hash2]: {testSubId_3: true},
|
|
156
158
|
},
|
|
157
159
|
})
|
|
@@ -165,23 +167,20 @@ describe('getProjectionState', () => {
|
|
|
165
167
|
})
|
|
166
168
|
|
|
167
169
|
// --- Test Unsubscribe ---
|
|
168
|
-
unsubscribe1() // Unsubscribes
|
|
169
|
-
expect(state.get().subscriptions[docHandle.documentId]?.[hash1]).toEqual({
|
|
170
|
-
testSubId_2: true,
|
|
171
|
-
testSubId_4: true,
|
|
172
|
-
})
|
|
170
|
+
unsubscribe1() // Unsubscribes first subscription consumer
|
|
171
|
+
expect(state.get().subscriptions[docHandle.documentId]?.[hash1]).toEqual({testSubId_2: true})
|
|
173
172
|
vi.advanceTimersByTime(PROJECTION_STATE_CLEAR_DELAY)
|
|
174
|
-
expect(state.get().subscriptions[docHandle.documentId]?.[hash1]).toEqual({
|
|
173
|
+
expect(state.get().subscriptions[docHandle.documentId]?.[hash1]).toEqual({testSubId_2: true})
|
|
175
174
|
expect(state.get().documentProjections[docHandle.documentId]?.[hash1]).toEqual(projection1)
|
|
176
175
|
|
|
177
|
-
unsubscribe3() //
|
|
176
|
+
unsubscribe3() // Also unsubscibes first projection subscription, should be removed
|
|
178
177
|
vi.advanceTimersByTime(PROJECTION_STATE_CLEAR_DELAY)
|
|
179
178
|
expect(state.get().subscriptions[docHandle.documentId]?.[hash1]).toBeUndefined()
|
|
180
179
|
expect(state.get().documentProjections[docHandle.documentId]?.[hash1]).toBeUndefined()
|
|
181
180
|
expect(state.get().subscriptions[docHandle.documentId]?.[hash2]).toEqual({testSubId_3: true})
|
|
182
181
|
expect(state.get().documentProjections[docHandle.documentId]?.[hash2]).toEqual(projection2)
|
|
183
182
|
|
|
184
|
-
unsubscribe2() // Unsubscribes
|
|
183
|
+
unsubscribe2() // Unsubscribes second projection subscription, should be removed
|
|
185
184
|
vi.advanceTimersByTime(PROJECTION_STATE_CLEAR_DELAY)
|
|
186
185
|
expect(state.get().subscriptions[docHandle.documentId]).toBeUndefined()
|
|
187
186
|
expect(state.get().documentProjections[docHandle.documentId]).toBeUndefined()
|
|
@@ -203,14 +202,14 @@ describe('getProjectionState', () => {
|
|
|
203
202
|
}))
|
|
204
203
|
|
|
205
204
|
const unsubscribe1 = projectionState.subscribe(vi.fn()) // Should use ID 2
|
|
206
|
-
const unsubscribe2 = projectionState.subscribe(vi.fn()) // Should
|
|
205
|
+
const unsubscribe2 = projectionState.subscribe(vi.fn()) // Should reuse ID 2
|
|
207
206
|
|
|
208
207
|
expect(state.get().values[docHandle.documentId]?.[hash]).toEqual({
|
|
209
208
|
data: initialData,
|
|
210
209
|
isPending: true,
|
|
211
210
|
})
|
|
212
211
|
|
|
213
|
-
unsubscribe1() // Unsubscribes
|
|
212
|
+
unsubscribe1() // Unsubscribes first subscription consumer
|
|
214
213
|
vi.advanceTimersByTime(PROJECTION_STATE_CLEAR_DELAY)
|
|
215
214
|
expect(state.get().values[docHandle.documentId]?.[hash]).toEqual({
|
|
216
215
|
data: initialData,
|
|
@@ -219,9 +218,10 @@ describe('getProjectionState', () => {
|
|
|
219
218
|
expect(Object.keys(state.get().subscriptions[docHandle.documentId]?.[hash] ?? {}).length).toBe(
|
|
220
219
|
1,
|
|
221
220
|
)
|
|
222
|
-
|
|
221
|
+
// should still have the same subscription ID since we're not unsubscribing the second consumer
|
|
222
|
+
expect(state.get().subscriptions[docHandle.documentId]?.[hash]).toEqual({testSubId_2: true})
|
|
223
223
|
|
|
224
|
-
unsubscribe2() // Unsubscribes
|
|
224
|
+
unsubscribe2() // Unsubscribes second subscription consumer
|
|
225
225
|
expect(state.get().values[docHandle.documentId]?.[hash]).toEqual({
|
|
226
226
|
data: initialData,
|
|
227
227
|
isPending: true,
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from '../store/createStateSourceAction'
|
|
13
13
|
import {hashString} from '../utils/hashString'
|
|
14
14
|
import {insecureRandomId} from '../utils/ids'
|
|
15
|
+
import {setCleanupTimeout} from '../utils/setCleanupTimeout'
|
|
15
16
|
import {projectionStore} from './projectionStore'
|
|
16
17
|
import {type ProjectionStoreState, type ProjectionValuePending} from './types'
|
|
17
18
|
import {PROJECTION_STATE_CLEAR_DELAY, STABLE_EMPTY_PROJECTION, validateProjection} from './util'
|
|
@@ -110,7 +111,7 @@ export const _getProjectionState = bindActionBySourceAndPerspective(
|
|
|
110
111
|
}))
|
|
111
112
|
|
|
112
113
|
return () => {
|
|
113
|
-
|
|
114
|
+
setCleanupTimeout(() => {
|
|
114
115
|
state.set('removeSubscription', (prev): Partial<ProjectionStoreState> => {
|
|
115
116
|
const documentSubscriptionsForHash = omit(
|
|
116
117
|
prev.subscriptions[documentId]?.[projectionHash],
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import {type ClientPerspective} from '@sanity/client'
|
|
2
|
-
import {DocumentId} from '@sanity/id-utils'
|
|
2
|
+
import {DocumentId, getPublishedId} from '@sanity/id-utils'
|
|
3
3
|
|
|
4
4
|
import {type ReleasePerspective} from '../config/sanityConfig'
|
|
5
|
-
import {getPublishedId} from '../utils/ids'
|
|
6
5
|
import {
|
|
7
6
|
type DocumentProjections,
|
|
8
7
|
type DocumentProjectionValues,
|
|
@@ -85,7 +84,7 @@ export function processProjectionQuery({
|
|
|
85
84
|
} = {}
|
|
86
85
|
|
|
87
86
|
for (const result of results) {
|
|
88
|
-
const originalId = getPublishedId(result._id)
|
|
87
|
+
const originalId = getPublishedId(DocumentId(result._id))
|
|
89
88
|
const hash = result.__projectionHash
|
|
90
89
|
|
|
91
90
|
if (!ids.has(originalId)) continue
|
package/src/projection/types.ts
CHANGED
package/src/query/queryStore.ts
CHANGED
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
import {type StoreState} from '../store/createStoreState'
|
|
46
46
|
import {defineStore, type StoreContext} from '../store/defineStore'
|
|
47
47
|
import {insecureRandomId} from '../utils/ids'
|
|
48
|
+
import {setCleanupTimeout} from '../utils/setCleanupTimeout'
|
|
48
49
|
import {
|
|
49
50
|
QUERY_STATE_CLEAR_DELAY,
|
|
50
51
|
QUERY_STORE_API_VERSION,
|
|
@@ -333,7 +334,7 @@ const _getQueryState = bindActionBySource(
|
|
|
333
334
|
|
|
334
335
|
return () => {
|
|
335
336
|
// this runs on unsubscribe
|
|
336
|
-
|
|
337
|
+
setCleanupTimeout(
|
|
337
338
|
() => state.set('removeSubscriber', removeSubscriber(key, subscriptionId)),
|
|
338
339
|
QUERY_STATE_CLEAR_DELAY,
|
|
339
340
|
)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {createSelector} from 'reselect'
|
|
2
2
|
|
|
3
|
-
import {type PerspectiveHandle} from '../config/sanityConfig'
|
|
4
|
-
import {
|
|
3
|
+
import {type DocumentSource, type PerspectiveHandle} from '../config/sanityConfig'
|
|
4
|
+
import {bindActionBySource, type BoundStoreAction} from '../store/createActionBinder'
|
|
5
5
|
import {createStateSourceAction, type SelectorContext} from '../store/createStateSourceAction'
|
|
6
6
|
/*
|
|
7
7
|
* Although this is an import dependency cycle, it is not a logical cycle:
|
|
@@ -26,7 +26,7 @@ const selectActiveReleases = (context: SelectorContext<ReleasesStoreState>) =>
|
|
|
26
26
|
context.state.activeReleases
|
|
27
27
|
const selectOptions = (
|
|
28
28
|
_context: SelectorContext<ReleasesStoreState>,
|
|
29
|
-
options: PerspectiveHandle & {projectId?: string; dataset?: string},
|
|
29
|
+
options: PerspectiveHandle & {projectId?: string; dataset?: string; source?: DocumentSource},
|
|
30
30
|
) => options
|
|
31
31
|
|
|
32
32
|
const memoizedOptionsSelector = createSelector(
|
|
@@ -103,12 +103,13 @@ let _boundGetPerspectiveState: BoundGetPerspectiveState | undefined
|
|
|
103
103
|
*
|
|
104
104
|
* @public
|
|
105
105
|
*/
|
|
106
|
-
export const getPerspectiveState: BoundGetPerspectiveState = (...
|
|
106
|
+
export const getPerspectiveState: BoundGetPerspectiveState = (instance, ...rest) => {
|
|
107
107
|
if (!_boundGetPerspectiveState) {
|
|
108
|
-
_boundGetPerspectiveState =
|
|
108
|
+
_boundGetPerspectiveState = bindActionBySource(
|
|
109
109
|
releasesStore,
|
|
110
110
|
_getPerspectiveStateSelector,
|
|
111
111
|
) as BoundGetPerspectiveState
|
|
112
112
|
}
|
|
113
|
-
|
|
113
|
+
// bindActionBySource keyFn destructures { source } from the first param, so pass {} when no options
|
|
114
|
+
return _boundGetPerspectiveState(instance, ...(rest.length ? rest : [{}]))
|
|
114
115
|
}
|