@sanity/sdk 2.5.0 → 2.7.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/index.d.ts +429 -27
- package/dist/index.js +657 -266
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
- package/src/_exports/index.ts +18 -3
- package/src/auth/authMode.test.ts +56 -0
- package/src/auth/authMode.ts +71 -0
- package/src/auth/authStore.test.ts +85 -4
- package/src/auth/authStore.ts +63 -125
- package/src/auth/authStrategy.ts +39 -0
- package/src/auth/dashboardAuth.ts +132 -0
- package/src/auth/standaloneAuth.ts +109 -0
- package/src/auth/studioAuth.ts +217 -0
- package/src/auth/studioModeAuth.test.ts +43 -1
- package/src/auth/studioModeAuth.ts +10 -1
- package/src/auth/subscribeToStateAndFetchCurrentUser.ts +21 -6
- package/src/client/clientStore.test.ts +45 -43
- package/src/client/clientStore.ts +23 -9
- package/src/config/loggingConfig.ts +149 -0
- package/src/config/sanityConfig.ts +82 -22
- package/src/projection/getProjectionState.ts +6 -5
- package/src/projection/projectionQuery.test.ts +38 -55
- package/src/projection/projectionQuery.ts +27 -31
- package/src/projection/projectionStore.test.ts +4 -4
- package/src/projection/projectionStore.ts +3 -2
- package/src/projection/resolveProjection.ts +2 -2
- package/src/projection/statusQuery.test.ts +35 -0
- package/src/projection/statusQuery.ts +71 -0
- package/src/projection/subscribeToStateAndFetchBatches.test.ts +63 -50
- package/src/projection/subscribeToStateAndFetchBatches.ts +106 -27
- package/src/projection/types.ts +12 -0
- package/src/projection/util.ts +0 -1
- package/src/query/queryStore.test.ts +64 -0
- package/src/query/queryStore.ts +33 -11
- package/src/releases/getPerspectiveState.test.ts +17 -14
- package/src/releases/getPerspectiveState.ts +58 -38
- package/src/releases/releasesStore.test.ts +59 -61
- package/src/releases/releasesStore.ts +21 -35
- package/src/releases/utils/isReleasePerspective.ts +7 -0
- package/src/store/createActionBinder.test.ts +211 -1
- package/src/store/createActionBinder.ts +102 -13
- package/src/store/createSanityInstance.test.ts +85 -1
- package/src/store/createSanityInstance.ts +55 -4
- package/src/utils/logger-usage-example.md +141 -0
- package/src/utils/logger.test.ts +757 -0
- package/src/utils/logger.ts +537 -0
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import {DocumentId} from '@sanity/id-utils'
|
|
1
2
|
import {describe, expect, it} from 'vitest'
|
|
2
3
|
|
|
3
4
|
import {createProjectionQuery, processProjectionQuery} from './projectionQuery'
|
|
4
5
|
|
|
5
6
|
describe('createProjectionQuery', () => {
|
|
6
7
|
it('creates a query and params for given ids and projections', () => {
|
|
7
|
-
const ids = new Set(['doc1', 'doc2'])
|
|
8
|
+
const ids = new Set(['doc1', 'doc2'].map(DocumentId))
|
|
8
9
|
const projectionHash = '{title, description}'
|
|
9
10
|
const documentProjections = {
|
|
10
11
|
doc1: {[projectionHash]: projectionHash},
|
|
@@ -15,11 +16,11 @@ describe('createProjectionQuery', () => {
|
|
|
15
16
|
expect(query).toMatch(/.*_id in \$__ids_.*/)
|
|
16
17
|
expect(Object.keys(params)).toHaveLength(1)
|
|
17
18
|
expect(params[`__ids_${projectionHash}`]).toBeDefined()
|
|
18
|
-
expect(params[`__ids_${projectionHash}`]).toHaveLength(
|
|
19
|
+
expect(params[`__ids_${projectionHash}`]).toHaveLength(2)
|
|
19
20
|
})
|
|
20
21
|
|
|
21
22
|
it('handles multiple different projections', () => {
|
|
22
|
-
const ids = new Set(['doc1', 'doc2'])
|
|
23
|
+
const ids = new Set(['doc1', 'doc2'].map(DocumentId))
|
|
23
24
|
const projectionHash1 = '{title, description}'
|
|
24
25
|
const projectionHash2 = '{name, age}'
|
|
25
26
|
const documentProjections = {
|
|
@@ -31,13 +32,13 @@ describe('createProjectionQuery', () => {
|
|
|
31
32
|
expect(query).toMatch(/.*_id in \$__ids_.*/)
|
|
32
33
|
expect(Object.keys(params)).toHaveLength(2)
|
|
33
34
|
expect(params[`__ids_${projectionHash1}`]).toBeDefined()
|
|
34
|
-
expect(params[`__ids_${projectionHash1}`]).toHaveLength(
|
|
35
|
+
expect(params[`__ids_${projectionHash1}`]).toHaveLength(1)
|
|
35
36
|
expect(params[`__ids_${projectionHash2}`]).toBeDefined()
|
|
36
|
-
expect(params[`__ids_${projectionHash2}`]).toHaveLength(
|
|
37
|
+
expect(params[`__ids_${projectionHash2}`]).toHaveLength(1)
|
|
37
38
|
})
|
|
38
39
|
|
|
39
40
|
it('filters out ids without projections', () => {
|
|
40
|
-
const ids = new Set(['doc1', 'doc2', 'doc3'])
|
|
41
|
+
const ids = new Set(['doc1', 'doc2', 'doc3'].map(DocumentId))
|
|
41
42
|
const projectionHash1 = '{title}'
|
|
42
43
|
// projectionHash2 missing intentionally
|
|
43
44
|
const projectionHash3 = '{name}'
|
|
@@ -51,9 +52,9 @@ describe('createProjectionQuery', () => {
|
|
|
51
52
|
expect(query).toMatch(/.*_id in \$__ids_.*/)
|
|
52
53
|
expect(Object.keys(params)).toHaveLength(2)
|
|
53
54
|
expect(params[`__ids_${projectionHash1}`]).toBeDefined()
|
|
54
|
-
expect(params[`__ids_${projectionHash1}`]).toHaveLength(
|
|
55
|
+
expect(params[`__ids_${projectionHash1}`]).toHaveLength(1)
|
|
55
56
|
expect(params[`__ids_${projectionHash3}`]).toBeDefined()
|
|
56
|
-
expect(params[`__ids_${projectionHash3}`]).toHaveLength(
|
|
57
|
+
expect(params[`__ids_${projectionHash3}`]).toHaveLength(1)
|
|
57
58
|
})
|
|
58
59
|
})
|
|
59
60
|
|
|
@@ -63,10 +64,9 @@ describe('processProjectionQuery', () => {
|
|
|
63
64
|
it('returns structure with empty object if no results found', () => {
|
|
64
65
|
const ids = new Set(['doc1'])
|
|
65
66
|
const result = processProjectionQuery({
|
|
66
|
-
projectId: 'p',
|
|
67
|
-
dataset: 'd',
|
|
68
67
|
ids,
|
|
69
68
|
results: [], // no results
|
|
69
|
+
perspective: 'published',
|
|
70
70
|
})
|
|
71
71
|
|
|
72
72
|
expect(result['doc1']).toEqual({})
|
|
@@ -85,10 +85,14 @@ describe('processProjectionQuery', () => {
|
|
|
85
85
|
]
|
|
86
86
|
|
|
87
87
|
const processed = processProjectionQuery({
|
|
88
|
-
projectId: 'p',
|
|
89
|
-
dataset: 'd',
|
|
90
88
|
ids,
|
|
91
89
|
results,
|
|
90
|
+
perspective: 'published',
|
|
91
|
+
documentStatuses: {
|
|
92
|
+
doc1: {
|
|
93
|
+
lastEditedPublishedAt: '2021-01-01',
|
|
94
|
+
},
|
|
95
|
+
},
|
|
92
96
|
})
|
|
93
97
|
|
|
94
98
|
expect(processed['doc1']?.[testProjectionHash]).toEqual({
|
|
@@ -116,10 +120,14 @@ describe('processProjectionQuery', () => {
|
|
|
116
120
|
]
|
|
117
121
|
|
|
118
122
|
const processed = processProjectionQuery({
|
|
119
|
-
projectId: 'p',
|
|
120
|
-
dataset: 'd',
|
|
121
123
|
ids: new Set(['doc1']),
|
|
122
124
|
results,
|
|
125
|
+
perspective: 'published',
|
|
126
|
+
documentStatuses: {
|
|
127
|
+
doc1: {
|
|
128
|
+
lastEditedPublishedAt: '2021-01-01',
|
|
129
|
+
},
|
|
130
|
+
},
|
|
123
131
|
})
|
|
124
132
|
|
|
125
133
|
expect(processed['doc1']?.[testProjectionHash]).toEqual({
|
|
@@ -134,66 +142,37 @@ describe('processProjectionQuery', () => {
|
|
|
134
142
|
})
|
|
135
143
|
})
|
|
136
144
|
|
|
137
|
-
it('handles
|
|
145
|
+
it('handles release perspective with all three status fields', () => {
|
|
138
146
|
const results = [
|
|
139
|
-
{
|
|
140
|
-
_id: 'drafts.doc1',
|
|
141
|
-
_type: 'document',
|
|
142
|
-
_updatedAt: '2021-01-02',
|
|
143
|
-
result: {title: 'Draft'},
|
|
144
|
-
__projectionHash: testProjectionHash,
|
|
145
|
-
},
|
|
146
147
|
{
|
|
147
148
|
_id: 'doc1',
|
|
148
149
|
_type: 'document',
|
|
149
|
-
_updatedAt: '2021-01-
|
|
150
|
-
result: {title: '
|
|
150
|
+
_updatedAt: '2021-01-03',
|
|
151
|
+
result: {title: 'Version'},
|
|
151
152
|
__projectionHash: testProjectionHash,
|
|
152
153
|
},
|
|
153
154
|
]
|
|
154
155
|
|
|
155
156
|
const processed = processProjectionQuery({
|
|
156
|
-
projectId: 'p',
|
|
157
|
-
dataset: 'd',
|
|
158
157
|
ids: new Set(['doc1']),
|
|
159
158
|
results,
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
data: {
|
|
164
|
-
title: 'Draft',
|
|
165
|
-
_status: {
|
|
159
|
+
perspective: {releaseName: 'release1'},
|
|
160
|
+
documentStatuses: {
|
|
161
|
+
doc1: {
|
|
166
162
|
lastEditedDraftAt: '2021-01-02',
|
|
167
163
|
lastEditedPublishedAt: '2021-01-01',
|
|
164
|
+
lastEditedVersionAt: '2021-01-03',
|
|
168
165
|
},
|
|
169
166
|
},
|
|
170
|
-
isPending: false,
|
|
171
|
-
})
|
|
172
|
-
})
|
|
173
|
-
|
|
174
|
-
it('uses published result when no draft exists', () => {
|
|
175
|
-
const results = [
|
|
176
|
-
{
|
|
177
|
-
_id: 'doc1',
|
|
178
|
-
_type: 'document',
|
|
179
|
-
_updatedAt: '2021-01-01',
|
|
180
|
-
result: {title: 'Published'},
|
|
181
|
-
__projectionHash: testProjectionHash,
|
|
182
|
-
},
|
|
183
|
-
]
|
|
184
|
-
|
|
185
|
-
const processed = processProjectionQuery({
|
|
186
|
-
projectId: 'p',
|
|
187
|
-
dataset: 'd',
|
|
188
|
-
ids: new Set(['doc1']),
|
|
189
|
-
results,
|
|
190
167
|
})
|
|
191
168
|
|
|
192
169
|
expect(processed['doc1']?.[testProjectionHash]).toEqual({
|
|
193
170
|
data: {
|
|
194
|
-
title: '
|
|
171
|
+
title: 'Version',
|
|
195
172
|
_status: {
|
|
173
|
+
lastEditedDraftAt: '2021-01-02',
|
|
196
174
|
lastEditedPublishedAt: '2021-01-01',
|
|
175
|
+
lastEditedVersionAt: '2021-01-03',
|
|
197
176
|
},
|
|
198
177
|
},
|
|
199
178
|
isPending: false,
|
|
@@ -221,10 +200,14 @@ describe('processProjectionQuery', () => {
|
|
|
221
200
|
]
|
|
222
201
|
|
|
223
202
|
const processed = processProjectionQuery({
|
|
224
|
-
projectId: 'p',
|
|
225
|
-
dataset: 'd',
|
|
226
203
|
ids: new Set(['doc1']),
|
|
227
204
|
results,
|
|
205
|
+
perspective: 'published',
|
|
206
|
+
documentStatuses: {
|
|
207
|
+
doc1: {
|
|
208
|
+
lastEditedPublishedAt: '2021-01-01',
|
|
209
|
+
},
|
|
210
|
+
},
|
|
228
211
|
})
|
|
229
212
|
|
|
230
213
|
expect(processed['doc1']?.[hash1]).toEqual({
|
|
@@ -1,5 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import {type ClientPerspective} from '@sanity/client'
|
|
2
|
+
import {DocumentId} from '@sanity/id-utils'
|
|
3
|
+
|
|
4
|
+
import {type ReleasePerspective} from '../config/sanityConfig'
|
|
5
|
+
import {getPublishedId} from '../utils/ids'
|
|
6
|
+
import {
|
|
7
|
+
type DocumentProjections,
|
|
8
|
+
type DocumentProjectionValues,
|
|
9
|
+
type ProjectionStoreState,
|
|
10
|
+
} from './types'
|
|
3
11
|
import {validateProjection} from './util'
|
|
4
12
|
|
|
5
13
|
export type ProjectionQueryResult = {
|
|
@@ -18,8 +26,8 @@ interface CreateProjectionQueryResult {
|
|
|
18
26
|
type ProjectionMap = Record<string, {projection: string; documentIds: Set<string>}>
|
|
19
27
|
|
|
20
28
|
export function createProjectionQuery(
|
|
21
|
-
documentIds: Set<
|
|
22
|
-
documentProjections: {[TDocumentId in
|
|
29
|
+
documentIds: Set<DocumentId>,
|
|
30
|
+
documentProjections: {[TDocumentId in DocumentId]?: DocumentProjections},
|
|
23
31
|
): CreateProjectionQueryResult {
|
|
24
32
|
const projections = Array.from(documentIds)
|
|
25
33
|
.flatMap((id) => {
|
|
@@ -48,11 +56,7 @@ export function createProjectionQuery(
|
|
|
48
56
|
|
|
49
57
|
const params = Object.fromEntries(
|
|
50
58
|
Object.entries(projections).map(([projectionHash, value]) => {
|
|
51
|
-
const idsInProjection = Array.from(value.documentIds).flatMap((id) =>
|
|
52
|
-
getPublishedId(id),
|
|
53
|
-
getDraftId(id),
|
|
54
|
-
])
|
|
55
|
-
|
|
59
|
+
const idsInProjection = Array.from(value.documentIds).flatMap((id) => DocumentId(id))
|
|
56
60
|
return [`__ids_${projectionHash}`, Array.from(idsInProjection)]
|
|
57
61
|
}),
|
|
58
62
|
)
|
|
@@ -61,28 +65,28 @@ export function createProjectionQuery(
|
|
|
61
65
|
}
|
|
62
66
|
|
|
63
67
|
interface ProcessProjectionQueryOptions {
|
|
64
|
-
projectId: string
|
|
65
|
-
dataset: string
|
|
66
68
|
ids: Set<string>
|
|
67
69
|
results: ProjectionQueryResult[]
|
|
70
|
+
documentStatuses?: ProjectionStoreState['documentStatuses']
|
|
71
|
+
perspective: ClientPerspective | ReleasePerspective
|
|
68
72
|
}
|
|
69
73
|
|
|
70
|
-
export function processProjectionQuery({
|
|
74
|
+
export function processProjectionQuery({
|
|
75
|
+
ids,
|
|
76
|
+
results,
|
|
77
|
+
documentStatuses,
|
|
78
|
+
}: ProcessProjectionQueryOptions): {
|
|
71
79
|
[TDocumentId in string]?: DocumentProjectionValues<Record<string, unknown>>
|
|
72
80
|
} {
|
|
73
81
|
const groupedResults: {
|
|
74
82
|
[docId: string]: {
|
|
75
|
-
[hash: string]:
|
|
76
|
-
draft?: ProjectionQueryResult
|
|
77
|
-
published?: ProjectionQueryResult
|
|
78
|
-
}
|
|
83
|
+
[hash: string]: ProjectionQueryResult | undefined
|
|
79
84
|
}
|
|
80
85
|
} = {}
|
|
81
86
|
|
|
82
87
|
for (const result of results) {
|
|
83
88
|
const originalId = getPublishedId(result._id)
|
|
84
89
|
const hash = result.__projectionHash
|
|
85
|
-
const isDraft = result._id.startsWith('drafts.')
|
|
86
90
|
|
|
87
91
|
if (!ids.has(originalId)) continue
|
|
88
92
|
|
|
@@ -90,14 +94,10 @@ export function processProjectionQuery({ids, results}: ProcessProjectionQueryOpt
|
|
|
90
94
|
groupedResults[originalId] = {}
|
|
91
95
|
}
|
|
92
96
|
if (!groupedResults[originalId][hash]) {
|
|
93
|
-
groupedResults[originalId][hash] =
|
|
97
|
+
groupedResults[originalId][hash] = undefined
|
|
94
98
|
}
|
|
95
99
|
|
|
96
|
-
|
|
97
|
-
groupedResults[originalId][hash].draft = result
|
|
98
|
-
} else {
|
|
99
|
-
groupedResults[originalId][hash].published = result
|
|
100
|
-
}
|
|
100
|
+
groupedResults[originalId][hash] = result
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
const finalValues: {
|
|
@@ -111,22 +111,18 @@ export function processProjectionQuery({ids, results}: ProcessProjectionQueryOpt
|
|
|
111
111
|
if (!projectionsForDoc) continue
|
|
112
112
|
|
|
113
113
|
for (const hash in projectionsForDoc) {
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
const projectionResultData = draft?.result ?? published?.result
|
|
114
|
+
const projectionResult = projectionsForDoc[hash]
|
|
115
|
+
const projectionResultData = projectionResult?.result
|
|
117
116
|
|
|
118
117
|
if (!projectionResultData) {
|
|
119
118
|
finalValues[originalId][hash] = {data: null, isPending: false}
|
|
120
119
|
continue
|
|
121
120
|
}
|
|
122
121
|
|
|
123
|
-
const
|
|
124
|
-
...(draft?._updatedAt && {lastEditedDraftAt: draft._updatedAt}),
|
|
125
|
-
...(published?._updatedAt && {lastEditedPublishedAt: published._updatedAt}),
|
|
126
|
-
}
|
|
122
|
+
const statusFromStore = documentStatuses?.[originalId]
|
|
127
123
|
|
|
128
124
|
finalValues[originalId][hash] = {
|
|
129
|
-
data: {...projectionResultData, _status},
|
|
125
|
+
data: {...projectionResultData, _status: statusFromStore},
|
|
130
126
|
isPending: false,
|
|
131
127
|
}
|
|
132
128
|
}
|
|
@@ -30,8 +30,8 @@ describe('projectionStore', () => {
|
|
|
30
30
|
instance,
|
|
31
31
|
{
|
|
32
32
|
name: 'p.d',
|
|
33
|
-
projectId: 'p',
|
|
34
|
-
|
|
33
|
+
source: {projectId: 'p', dataset: 'd'},
|
|
34
|
+
perspective: 'drafts',
|
|
35
35
|
},
|
|
36
36
|
projectionStore,
|
|
37
37
|
)
|
|
@@ -42,8 +42,8 @@ describe('projectionStore', () => {
|
|
|
42
42
|
state,
|
|
43
43
|
key: {
|
|
44
44
|
name: 'p.d',
|
|
45
|
-
projectId: 'p',
|
|
46
|
-
|
|
45
|
+
source: {projectId: 'p', dataset: 'd'},
|
|
46
|
+
perspective: 'drafts',
|
|
47
47
|
},
|
|
48
48
|
})
|
|
49
49
|
|
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import {type
|
|
1
|
+
import {type BoundPerspectiveKey} from '../store/createActionBinder'
|
|
2
2
|
import {defineStore} from '../store/defineStore'
|
|
3
3
|
import {subscribeToStateAndFetchBatches} from './subscribeToStateAndFetchBatches'
|
|
4
4
|
import {type ProjectionStoreState} from './types'
|
|
5
5
|
|
|
6
|
-
export const projectionStore = defineStore<ProjectionStoreState,
|
|
6
|
+
export const projectionStore = defineStore<ProjectionStoreState, BoundPerspectiveKey>({
|
|
7
7
|
name: 'Projection',
|
|
8
8
|
getInitialState() {
|
|
9
9
|
return {
|
|
10
10
|
values: {},
|
|
11
11
|
documentProjections: {},
|
|
12
12
|
subscriptions: {},
|
|
13
|
+
documentStatuses: {},
|
|
13
14
|
}
|
|
14
15
|
},
|
|
15
16
|
initialize(context) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {type SanityProjectionResult} from 'groq'
|
|
2
2
|
import {filter, firstValueFrom} from 'rxjs'
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {bindActionBySourceAndPerspective} from '../store/createActionBinder'
|
|
5
5
|
import {type SanityInstance} from '../store/createSanityInstance'
|
|
6
6
|
import {getProjectionState, type ProjectionOptions} from './getProjectionState'
|
|
7
7
|
import {projectionStore} from './projectionStore'
|
|
@@ -38,7 +38,7 @@ export function resolveProjection(
|
|
|
38
38
|
/**
|
|
39
39
|
* @beta
|
|
40
40
|
*/
|
|
41
|
-
const _resolveProjection =
|
|
41
|
+
const _resolveProjection = bindActionBySourceAndPerspective(
|
|
42
42
|
projectionStore,
|
|
43
43
|
(
|
|
44
44
|
{instance}: {instance: SanityInstance},
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import {DocumentId, getVersionId} from '@sanity/id-utils'
|
|
2
|
+
import {describe, expect, it} from 'vitest'
|
|
3
|
+
|
|
4
|
+
import {buildStatusQueryIds, processStatusQueryResults} from './statusQuery'
|
|
5
|
+
|
|
6
|
+
describe('buildStatusQueryIds', () => {
|
|
7
|
+
it('includes draft and published ids only when perspective is not a release', () => {
|
|
8
|
+
const ids = new Set(['doc1'].map(DocumentId))
|
|
9
|
+
const result = buildStatusQueryIds(ids, 'published')
|
|
10
|
+
expect(result).toContain('drafts.doc1')
|
|
11
|
+
expect(result).toContain('doc1')
|
|
12
|
+
expect(result).toHaveLength(2)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('includes version ids when perspective is a release', () => {
|
|
16
|
+
const ids = new Set(['doc1'].map(DocumentId))
|
|
17
|
+
const perspective = {releaseName: 'myRelease'}
|
|
18
|
+
const result = buildStatusQueryIds(ids, perspective)
|
|
19
|
+
expect(result).toContain('drafts.doc1')
|
|
20
|
+
expect(result).toContain('doc1')
|
|
21
|
+
expect(result).toContain(getVersionId(DocumentId('doc1'), 'myRelease'))
|
|
22
|
+
expect(result).toHaveLength(3)
|
|
23
|
+
})
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
describe('processStatusQueryResults', () => {
|
|
27
|
+
it('sets lastEditedVersionAt when result _id is a version id', () => {
|
|
28
|
+
const versionId = getVersionId(DocumentId('doc1'), 'myRelease')
|
|
29
|
+
const results = [{_id: versionId, _updatedAt: '2025-01-01T12:00:00Z'}]
|
|
30
|
+
const documentStatuses = processStatusQueryResults(results)
|
|
31
|
+
expect(documentStatuses['doc1']).toEqual({
|
|
32
|
+
lastEditedVersionAt: '2025-01-01T12:00:00Z',
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
})
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DocumentId,
|
|
3
|
+
getDraftId,
|
|
4
|
+
getPublishedId,
|
|
5
|
+
getVersionId,
|
|
6
|
+
isDraftId,
|
|
7
|
+
isPublishedId,
|
|
8
|
+
isVersionId,
|
|
9
|
+
} from '@sanity/id-utils'
|
|
10
|
+
|
|
11
|
+
import {isReleasePerspective} from '../releases/utils/isReleasePerspective'
|
|
12
|
+
import {type BoundPerspectiveKey} from '../store/createActionBinder'
|
|
13
|
+
import {type ProjectionStoreState} from './types'
|
|
14
|
+
|
|
15
|
+
interface StatusQueryResult {
|
|
16
|
+
_id: string
|
|
17
|
+
_updatedAt: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Builds an array of document IDs to query for status information using the "raw" perspective.
|
|
22
|
+
* Includes draft, published, and version IDs as needed.
|
|
23
|
+
*/
|
|
24
|
+
export function buildStatusQueryIds(
|
|
25
|
+
documentIds: Set<string>,
|
|
26
|
+
perspective: BoundPerspectiveKey['perspective'],
|
|
27
|
+
): string[] {
|
|
28
|
+
const ids: string[] = []
|
|
29
|
+
const releaseName = isReleasePerspective(perspective) ? perspective.releaseName : null
|
|
30
|
+
|
|
31
|
+
for (const id of documentIds) {
|
|
32
|
+
const publishedId = getPublishedId(DocumentId(id))
|
|
33
|
+
const draftId = getDraftId(publishedId)
|
|
34
|
+
|
|
35
|
+
// Always query for draft and published versions
|
|
36
|
+
ids.push(draftId, publishedId)
|
|
37
|
+
|
|
38
|
+
// If it's a release perspective, also query for the version
|
|
39
|
+
if (releaseName) {
|
|
40
|
+
ids.push(getVersionId(publishedId, releaseName))
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return ids
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Processes status query results into documentStatuses (same shape as _status returned to users).
|
|
49
|
+
*/
|
|
50
|
+
export function processStatusQueryResults(
|
|
51
|
+
results: StatusQueryResult[],
|
|
52
|
+
): ProjectionStoreState['documentStatuses'] {
|
|
53
|
+
const documentStatuses: ProjectionStoreState['documentStatuses'] = {}
|
|
54
|
+
|
|
55
|
+
for (const result of results) {
|
|
56
|
+
const id = DocumentId(result._id)
|
|
57
|
+
const updatedAt = result._updatedAt
|
|
58
|
+
const publishedId = getPublishedId(id)
|
|
59
|
+
const statusData = documentStatuses[publishedId] ?? {}
|
|
60
|
+
if (isDraftId(id)) {
|
|
61
|
+
statusData.lastEditedDraftAt = updatedAt
|
|
62
|
+
} else if (isVersionId(id)) {
|
|
63
|
+
statusData.lastEditedVersionAt = updatedAt
|
|
64
|
+
} else if (isPublishedId(id)) {
|
|
65
|
+
statusData.lastEditedPublishedAt = updatedAt
|
|
66
|
+
}
|
|
67
|
+
documentStatuses[publishedId] = statusData
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return documentStatuses
|
|
71
|
+
}
|
|
@@ -15,13 +15,18 @@ vi.mock('../query/queryStore')
|
|
|
15
15
|
describe('subscribeToStateAndFetchBatches', () => {
|
|
16
16
|
let instance: SanityInstance
|
|
17
17
|
let state: StoreState<ProjectionStoreState>
|
|
18
|
-
const key = {
|
|
18
|
+
const key = {
|
|
19
|
+
name: 'test.test:drafts',
|
|
20
|
+
source: {projectId: 'test', dataset: 'test'},
|
|
21
|
+
perspective: 'drafts' as const,
|
|
22
|
+
}
|
|
19
23
|
|
|
20
24
|
beforeEach(() => {
|
|
21
25
|
vi.clearAllMocks()
|
|
22
26
|
instance = createSanityInstance({projectId: 'test', dataset: 'test'})
|
|
23
27
|
state = createStoreState<ProjectionStoreState>({
|
|
24
28
|
documentProjections: {},
|
|
29
|
+
documentStatuses: {},
|
|
25
30
|
subscriptions: {},
|
|
26
31
|
values: {},
|
|
27
32
|
})
|
|
@@ -65,19 +70,14 @@ describe('subscribeToStateAndFetchBatches', () => {
|
|
|
65
70
|
// Wait for debounce
|
|
66
71
|
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
67
72
|
|
|
68
|
-
//
|
|
69
|
-
expect(getQueryState).toHaveBeenCalledTimes(
|
|
73
|
+
// only 2 calls (one for projection, one for status) even though we added 2 subscriptions
|
|
74
|
+
expect(getQueryState).toHaveBeenCalledTimes(2)
|
|
70
75
|
expect(getQueryState).toHaveBeenCalledWith(
|
|
71
76
|
instance,
|
|
72
77
|
expect.objectContaining({
|
|
73
|
-
|
|
78
|
+
perspective: 'drafts',
|
|
74
79
|
params: {
|
|
75
|
-
[`__ids_${projectionHash}`]: expect.arrayContaining([
|
|
76
|
-
'doc1',
|
|
77
|
-
'drafts.doc1',
|
|
78
|
-
'doc2',
|
|
79
|
-
'drafts.doc2',
|
|
80
|
-
]),
|
|
80
|
+
[`__ids_${projectionHash}`]: expect.arrayContaining(['doc1', 'doc2']),
|
|
81
81
|
},
|
|
82
82
|
}),
|
|
83
83
|
)
|
|
@@ -87,47 +87,43 @@ describe('subscribeToStateAndFetchBatches', () => {
|
|
|
87
87
|
|
|
88
88
|
it('processes query results and updates state with resolved values', async () => {
|
|
89
89
|
const teardown = vi.fn()
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
observable
|
|
97
|
-
|
|
90
|
+
const projectionObservers: Observer<ProjectionQueryResult[] | undefined>[] = []
|
|
91
|
+
const statusObservers: Observer<{_id: string; _updatedAt: string}[] | undefined>[] = []
|
|
92
|
+
|
|
93
|
+
vi.mocked(getQueryState).mockImplementation((_, options) => {
|
|
94
|
+
const isStatusQuery = options.perspective === 'raw'
|
|
95
|
+
const observers = isStatusQuery ? statusObservers : projectionObservers
|
|
96
|
+
const observable = new Observable<unknown>((observer) => {
|
|
97
|
+
observers.push(observer as Observer<ProjectionQueryResult[] | undefined>)
|
|
98
|
+
return teardown
|
|
99
|
+
})
|
|
100
|
+
return {
|
|
101
|
+
getCurrent: () => undefined,
|
|
102
|
+
observable,
|
|
103
|
+
} as StateSource<ProjectionQueryResult[] | undefined>
|
|
104
|
+
})
|
|
98
105
|
|
|
99
106
|
const subscription = subscribeToStateAndFetchBatches({instance, state, key})
|
|
100
107
|
const projection = '{title}'
|
|
101
108
|
const projectionHash = hashString(projection)
|
|
102
109
|
|
|
103
|
-
expect(subscriber).not.toHaveBeenCalled()
|
|
104
|
-
|
|
105
110
|
// Add a subscription
|
|
106
111
|
state.set('addSubscription', {
|
|
107
112
|
documentProjections: {doc1: {[projectionHash]: projection}},
|
|
108
113
|
subscriptions: {doc1: {[projectionHash]: {sub1: true}}},
|
|
109
114
|
})
|
|
110
115
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
// Wait for debounce
|
|
116
|
+
// Wait for debounce (combineLatest subscribes to both projection and status)
|
|
114
117
|
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
115
118
|
|
|
116
|
-
expect(
|
|
119
|
+
expect(projectionObservers.length).toBe(1)
|
|
120
|
+
expect(statusObservers.length).toBe(1)
|
|
117
121
|
expect(teardown).not.toHaveBeenCalled()
|
|
118
122
|
|
|
119
|
-
const [observer] = subscriber.mock.lastCall!
|
|
120
|
-
|
|
121
123
|
const timestamp = new Date().toISOString()
|
|
122
124
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
_id: 'doc1',
|
|
126
|
-
_type: 'doc',
|
|
127
|
-
_updatedAt: timestamp,
|
|
128
|
-
result: {title: 'resolved'},
|
|
129
|
-
__projectionHash: projectionHash,
|
|
130
|
-
},
|
|
125
|
+
// Emit projection results first
|
|
126
|
+
projectionObservers[0]!.next([
|
|
131
127
|
{
|
|
132
128
|
_id: 'drafts.doc1',
|
|
133
129
|
_type: 'doc',
|
|
@@ -137,6 +133,12 @@ describe('subscribeToStateAndFetchBatches', () => {
|
|
|
137
133
|
},
|
|
138
134
|
])
|
|
139
135
|
|
|
136
|
+
// Emit status results (raw query returns _id, _updatedAt per document variant)
|
|
137
|
+
statusObservers[0]!.next([
|
|
138
|
+
{_id: 'doc1', _updatedAt: timestamp},
|
|
139
|
+
{_id: 'drafts.doc1', _updatedAt: timestamp},
|
|
140
|
+
])
|
|
141
|
+
|
|
140
142
|
const {values} = state.get()
|
|
141
143
|
expect(values['doc1']?.[projectionHash]).toEqual({
|
|
142
144
|
isPending: false,
|
|
@@ -247,11 +249,9 @@ describe('subscribeToStateAndFetchBatches', () => {
|
|
|
247
249
|
|
|
248
250
|
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
249
251
|
|
|
250
|
-
// Expected calls:
|
|
251
|
-
//
|
|
252
|
-
|
|
253
|
-
// 3. Debounced fetch for (doc1, hash1) AND (doc2, hash2)
|
|
254
|
-
expect(getQueryState).toHaveBeenCalledTimes(initialQueryCallCount + 1)
|
|
252
|
+
// Expected calls: initial batch has 2 (projection + status). After adding doc2,
|
|
253
|
+
// debounced fetch triggers 2 more (projection + status for new batch).
|
|
254
|
+
expect(getQueryState).toHaveBeenCalledTimes(initialQueryCallCount + 2)
|
|
255
255
|
// Abort should have been called because the required projections changed
|
|
256
256
|
expect(abortSpy).toHaveBeenCalled()
|
|
257
257
|
|
|
@@ -259,13 +259,21 @@ describe('subscribeToStateAndFetchBatches', () => {
|
|
|
259
259
|
})
|
|
260
260
|
|
|
261
261
|
it('processes and applies fetch results correctly', async () => {
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
vi.mocked(getQueryState).
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
262
|
+
const projectionObservers: Observer<ProjectionQueryResult[] | undefined>[] = []
|
|
263
|
+
const statusObservers: Observer<{_id: string; _updatedAt: string}[] | undefined>[] = []
|
|
264
|
+
|
|
265
|
+
vi.mocked(getQueryState).mockImplementation((_, options) => {
|
|
266
|
+
const isStatusQuery = options.perspective === 'raw'
|
|
267
|
+
const observers = isStatusQuery ? statusObservers : projectionObservers
|
|
268
|
+
const observable = new Observable<unknown>((observer) => {
|
|
269
|
+
observers.push(observer as Observer<ProjectionQueryResult[] | undefined>)
|
|
270
|
+
return () => {}
|
|
271
|
+
})
|
|
272
|
+
return {
|
|
273
|
+
getCurrent: () => undefined,
|
|
274
|
+
observable,
|
|
275
|
+
} as StateSource<ProjectionQueryResult[] | undefined>
|
|
276
|
+
})
|
|
269
277
|
|
|
270
278
|
const subscription = subscribeToStateAndFetchBatches({instance, state, key})
|
|
271
279
|
const projection = '{title, description}'
|
|
@@ -280,12 +288,12 @@ describe('subscribeToStateAndFetchBatches', () => {
|
|
|
280
288
|
|
|
281
289
|
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
282
290
|
|
|
283
|
-
expect(
|
|
284
|
-
|
|
291
|
+
expect(projectionObservers.length).toBe(1)
|
|
292
|
+
expect(statusObservers.length).toBe(1)
|
|
285
293
|
|
|
286
294
|
// Emit fetch results
|
|
287
295
|
const timestamp = '2024-01-01T00:00:00Z'
|
|
288
|
-
|
|
296
|
+
projectionObservers[0]!.next([
|
|
289
297
|
{
|
|
290
298
|
_id: 'doc1',
|
|
291
299
|
_type: 'test',
|
|
@@ -294,13 +302,18 @@ describe('subscribeToStateAndFetchBatches', () => {
|
|
|
294
302
|
__projectionHash: projectionHash,
|
|
295
303
|
},
|
|
296
304
|
])
|
|
305
|
+
statusObservers[0]!.next([
|
|
306
|
+
{_id: 'doc1', _updatedAt: timestamp},
|
|
307
|
+
{_id: 'drafts.doc1', _updatedAt: timestamp},
|
|
308
|
+
])
|
|
297
309
|
|
|
298
|
-
// Check that the state was updated
|
|
310
|
+
// Check that the state was updated (status query provides both draft and published _updatedAt)
|
|
299
311
|
expect(state.get().values['doc1']?.[projectionHash]).toEqual({
|
|
300
312
|
data: expect.objectContaining({
|
|
301
313
|
title: 'Test Document',
|
|
302
314
|
description: 'Test Description',
|
|
303
315
|
_status: {
|
|
316
|
+
lastEditedDraftAt: timestamp,
|
|
304
317
|
lastEditedPublishedAt: timestamp,
|
|
305
318
|
},
|
|
306
319
|
}),
|