@sanity/sdk 3.0.0-rc.0 → 3.0.0-rc.1
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/createGroqSearchFilter.d.ts +1147 -0
- package/dist/_chunks-dts/createGroqSearchFilter.d.ts.map +1 -0
- package/dist/_chunks-es/_internal.js +134 -0
- package/dist/_chunks-es/_internal.js.map +1 -0
- package/dist/_chunks-es/createGroqSearchFilter.js +1864 -0
- package/dist/_chunks-es/createGroqSearchFilter.js.map +1 -0
- package/dist/_chunks-es/telemetryManager.js +93 -0
- package/dist/_chunks-es/telemetryManager.js.map +1 -0
- package/dist/_exports/_internal.d.ts +95 -0
- package/dist/_exports/_internal.d.ts.map +1 -0
- package/dist/_exports/_internal.js +24 -0
- package/dist/_exports/_internal.js.map +1 -0
- package/dist/index.d.ts +1079 -1076
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1967 -2302
- package/dist/index.js.map +1 -1
- package/package.json +48 -47
- package/src/_exports/_internal.ts +26 -0
- package/src/_exports/index.ts +120 -11
- package/src/access/checkPermissions.test.ts +55 -0
- package/src/access/checkPermissions.ts +54 -0
- package/src/agent/agentActions.test.ts +16 -60
- package/src/agent/agentActions.ts +25 -38
- package/src/applications/applications.test-d.ts +83 -0
- package/src/applications/applications.test.ts +130 -0
- package/src/applications/applications.ts +364 -0
- package/src/applications/userApplications.test.ts +126 -0
- package/src/applications/userApplications.ts +261 -0
- package/src/auth/authLogger.ts +30 -0
- package/src/auth/authMode.test.ts +25 -0
- package/src/auth/authMode.ts +6 -3
- package/src/auth/authStore.test.ts +194 -13
- package/src/auth/authStore.ts +59 -26
- package/src/auth/getOrganizationVerificationState.test.ts +18 -17
- package/src/auth/getOrganizationVerificationState.ts +4 -4
- package/src/auth/handleAuthCallback.test.ts +35 -1
- package/src/auth/handleAuthCallback.ts +25 -6
- package/src/auth/logout.test.ts +75 -2
- package/src/auth/logout.ts +23 -4
- package/src/auth/refreshStampedToken.test.ts +219 -469
- package/src/auth/refreshStampedToken.ts +79 -195
- package/src/auth/studioAuth.ts +1 -1
- package/src/auth/studioModeAuth.ts +2 -1
- package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +146 -13
- package/src/auth/subscribeToStateAndFetchCurrentUser.ts +37 -6
- package/src/auth/subscribeToStorageEventsAndSetToken.test.ts +2 -0
- package/src/client/clientStore.test.ts +62 -37
- package/src/client/clientStore.ts +45 -75
- package/src/client/liveEvents.test.ts +151 -0
- package/src/client/liveEvents.ts +107 -0
- package/src/comlink/controller/actions/destroyController.test.ts +4 -1
- package/src/comlink/controller/actions/getOrCreateChannel.test.ts +4 -1
- package/src/comlink/controller/actions/getOrCreateChannel.ts +2 -2
- package/src/comlink/controller/actions/getOrCreateController.test.ts +4 -1
- package/src/comlink/controller/actions/releaseChannel.test.ts +1 -1
- package/src/comlink/controller/comlinkControllerStore.test.ts +4 -1
- package/src/comlink/node/actions/getOrCreateNode.test.ts +9 -3
- package/src/comlink/node/actions/getOrCreateNode.ts +2 -2
- package/src/comlink/node/actions/releaseNode.test.ts +7 -4
- package/src/comlink/node/comlinkNodeStore.test.ts +2 -2
- package/src/comlink/node/getNodeState.test.ts +1 -1
- package/src/comlink/node/getNodeState.ts +2 -1
- package/src/config/__tests__/handles.test.ts +18 -12
- package/src/config/handles.ts +25 -7
- package/src/config/sanityConfig.ts +109 -79
- package/src/datasets/datasets.test.ts +2 -2
- package/src/datasets/datasets.ts +10 -4
- package/src/document/actions.test.ts +115 -33
- package/src/document/actions.ts +212 -15
- package/src/document/applyDocumentActions.test.ts +38 -12
- package/src/document/applyDocumentActions.ts +30 -9
- package/src/document/documentConstants.ts +7 -0
- package/src/document/documentStore.concurrency.test.ts +1306 -0
- package/src/document/documentStore.test.ts +673 -278
- package/src/document/documentStore.ts +162 -71
- package/src/document/events.test.ts +57 -2
- package/src/document/events.ts +82 -26
- package/src/document/listen.test.ts +56 -0
- package/src/document/listen.ts +74 -21
- package/src/document/listenerEventOperators.test.ts +311 -0
- package/src/document/listenerEventOperators.ts +217 -0
- package/src/document/patchOperations.test.ts +49 -2
- package/src/document/patchOperations.ts +50 -0
- package/src/document/permissions.test.ts +105 -60
- package/src/document/permissions.ts +44 -19
- package/src/document/processActions/create.ts +138 -0
- package/src/document/processActions/delete.ts +100 -0
- package/src/document/processActions/discard.ts +63 -0
- package/src/document/processActions/edit.ts +154 -0
- package/src/document/processActions/processActions.ts +218 -0
- package/src/document/processActions/publish.ts +120 -0
- 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 +308 -0
- package/src/document/processActions/unpublish.ts +85 -0
- package/src/document/processActions.test.ts +1129 -121
- package/src/document/reducers.test.ts +639 -64
- package/src/document/reducers.ts +187 -48
- package/src/document/resourceRules.test.ts +178 -0
- package/src/document/resourceRules.ts +117 -0
- package/src/document/sharedListener.test.ts +16 -14
- package/src/document/sharedListener.ts +24 -8
- package/src/document/util.ts +14 -0
- package/src/favorites/favorites.test.ts +5 -6
- package/src/installations/installations.test-d.ts +68 -0
- package/src/installations/installations.test.ts +62 -0
- package/src/installations/installations.ts +202 -0
- package/src/organization/organization.test-d.ts +36 -0
- package/src/organization/organization.test.ts +138 -0
- package/src/organization/organization.ts +138 -0
- package/src/organizations/organizations.test-d.ts +56 -0
- package/src/organizations/organizations.test.ts +150 -0
- package/src/organizations/organizations.ts +107 -0
- package/src/presence/bifurTransport.test.ts +46 -6
- package/src/presence/bifurTransport.ts +19 -2
- package/src/presence/presenceStore.test.ts +94 -23
- package/src/presence/presenceStore.ts +85 -29
- package/src/preview/getPreviewState.test.ts +137 -0
- package/src/preview/getPreviewState.ts +69 -0
- package/src/preview/previewProjectionUtils.test.ts +9 -22
- package/src/preview/previewProjectionUtils.ts +7 -3
- package/src/preview/resolvePreview.test.ts +64 -0
- package/src/preview/resolvePreview.ts +43 -0
- package/src/preview/types.ts +10 -0
- package/src/project/project.test-d.ts +37 -0
- package/src/project/project.test.ts +157 -12
- package/src/project/project.ts +138 -25
- package/src/projection/getProjectionState.test.ts +18 -20
- package/src/projection/getProjectionState.ts +23 -3
- package/src/projection/projectionQuery.ts +5 -5
- package/src/projection/projectionStore.test.ts +1 -1
- package/src/projection/resolveProjection.test.ts +1 -2
- package/src/projection/resolveProjection.ts +16 -0
- package/src/projection/subscribeToStateAndFetchBatches.test.ts +1 -1
- package/src/projection/subscribeToStateAndFetchBatches.ts +32 -23
- package/src/projection/types.ts +8 -0
- package/src/projects/projects.test-d.ts +32 -0
- package/src/projects/projects.test.ts +108 -42
- package/src/projects/projects.ts +63 -27
- package/src/query/queryStore.test.ts +259 -123
- package/src/query/queryStore.ts +78 -63
- package/src/query/reducers.test.ts +0 -42
- package/src/query/reducers.ts +2 -18
- package/src/releases/getPerspectiveState.test.ts +17 -23
- package/src/releases/getPerspectiveState.ts +8 -20
- package/src/releases/observeReleases.test.ts +127 -0
- package/src/releases/observeReleases.ts +102 -0
- package/src/releases/releasesStore.test.ts +100 -53
- package/src/releases/releasesStore.ts +69 -44
- package/src/releases/utils/sortReleases.test.ts +38 -2
- package/src/releases/utils/sortReleases.ts +15 -10
- package/src/store/createActionBinder.test.ts +87 -90
- package/src/store/createActionBinder.ts +79 -25
- package/src/store/createSanityInstance.test.ts +83 -13
- package/src/store/createSanityInstance.ts +77 -2
- package/src/store/createStateSourceAction.test.ts +64 -2
- package/src/store/createStateSourceAction.ts +39 -44
- package/src/store/createStoreInstance.test.ts +4 -2
- package/src/store/fetcherStore.test-d.ts +121 -0
- package/src/store/fetcherStore.test.ts +299 -0
- package/src/store/fetcherStore.ts +801 -0
- package/src/telemetry/environment.test.ts +119 -0
- package/src/telemetry/environment.ts +92 -0
- package/src/telemetry/events.ts +42 -0
- package/src/telemetry/initTelemetry.test.ts +432 -0
- package/src/telemetry/initTelemetry.ts +218 -0
- package/src/telemetry/telemetryManager.test.ts +327 -0
- package/src/telemetry/telemetryManager.ts +199 -0
- package/src/users/reducers.test.ts +6 -1
- package/src/users/reducers.ts +5 -6
- package/src/users/types.ts +4 -4
- package/src/users/usersStore.test.ts +16 -12
- package/src/users/usersStore.ts +5 -1
- package/src/utils/buildQuery.test.ts +14 -0
- package/src/utils/buildQuery.ts +17 -0
- package/src/utils/createFetcherStore.test.ts +7 -5
- package/src/utils/createFetcherStore.ts +8 -5
- 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/includeShape.ts +36 -0
- package/src/utils/isImportError.test.ts +72 -0
- package/src/utils/isImportError.ts +34 -0
- package/src/utils/logger.test.ts +12 -0
- package/src/utils/logger.ts +8 -3
- package/src/utils/object.test.ts +95 -0
- package/src/utils/object.ts +142 -0
- package/src/utils/setCleanupTimeout.ts +37 -0
- package/src/document/processActions.ts +0 -707
- package/src/utils/defineIntent.test.ts +0 -477
- package/src/utils/defineIntent.ts +0 -244
|
@@ -1,33 +1,178 @@
|
|
|
1
1
|
import {type SanityClient} from '@sanity/client'
|
|
2
2
|
import {of} from 'rxjs'
|
|
3
|
-
import {describe, it} from 'vitest'
|
|
3
|
+
import {afterEach, beforeEach, describe, it} from 'vitest'
|
|
4
4
|
|
|
5
5
|
import {getClientState} from '../client/clientStore'
|
|
6
|
-
import {
|
|
6
|
+
import {projects} from '../projects/projects'
|
|
7
|
+
import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
|
|
7
8
|
import {type StateSource} from '../store/createStateSourceAction'
|
|
8
|
-
import {
|
|
9
|
+
import {getProjectCacheKey, project} from './project'
|
|
9
10
|
|
|
10
11
|
vi.mock('../client/clientStore')
|
|
11
12
|
|
|
12
13
|
describe('project', () => {
|
|
13
|
-
|
|
14
|
-
const instance = createSanityInstance()
|
|
14
|
+
let instance: SanityInstance
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
instance = createSanityInstance({projectId: 'p', dataset: 'd'})
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
instance.dispose()
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('calls `client.observable.request` against `/projects/<id>` and returns the result', async () => {
|
|
25
|
+
const detail = {id: 'a'}
|
|
26
|
+
const request = vi.fn().mockReturnValue(of(detail))
|
|
18
27
|
|
|
19
28
|
const mockClient = {
|
|
20
|
-
observable: {
|
|
21
|
-
|
|
29
|
+
observable: {request} as unknown as SanityClient['observable'],
|
|
30
|
+
} as SanityClient
|
|
31
|
+
|
|
32
|
+
vi.mocked(getClientState).mockReturnValue({
|
|
33
|
+
observable: of(mockClient),
|
|
34
|
+
} as StateSource<SanityClient>)
|
|
35
|
+
|
|
36
|
+
const result = await project.resolveState(instance, {projectId: 'a'})
|
|
37
|
+
expect(result).toEqual(detail)
|
|
38
|
+
expect(request).toHaveBeenCalledWith({
|
|
39
|
+
uri: '/projects/a',
|
|
40
|
+
query: {includeMembers: 'true', includeFeatures: 'true'},
|
|
41
|
+
tag: 'projects.get',
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('serializes query params (booleans → strings) and respects flags', async () => {
|
|
46
|
+
const request = vi.fn().mockReturnValue(of({id: 'a'}))
|
|
47
|
+
const mockClient = {
|
|
48
|
+
observable: {request} as unknown as SanityClient['observable'],
|
|
49
|
+
} as SanityClient
|
|
50
|
+
|
|
51
|
+
vi.mocked(getClientState).mockReturnValue({
|
|
52
|
+
observable: of(mockClient),
|
|
53
|
+
} as StateSource<SanityClient>)
|
|
54
|
+
|
|
55
|
+
await project.resolveState(instance, {
|
|
56
|
+
projectId: 'a',
|
|
57
|
+
includeMembers: false,
|
|
58
|
+
includeFeatures: false,
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
expect(request).toHaveBeenCalledWith({
|
|
62
|
+
uri: '/projects/a',
|
|
63
|
+
query: {
|
|
64
|
+
includeMembers: 'false',
|
|
65
|
+
includeFeatures: 'false',
|
|
22
66
|
},
|
|
67
|
+
tag: 'projects.get',
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('falls back to the instance projectId when none is provided in options', async () => {
|
|
72
|
+
const request = vi.fn().mockReturnValue(of({id: 'p'}))
|
|
73
|
+
const mockClient = {
|
|
74
|
+
observable: {request} as unknown as SanityClient['observable'],
|
|
75
|
+
} as SanityClient
|
|
76
|
+
|
|
77
|
+
vi.mocked(getClientState).mockReturnValue({
|
|
78
|
+
observable: of(mockClient),
|
|
79
|
+
} as StateSource<SanityClient>)
|
|
80
|
+
|
|
81
|
+
await project.resolveState(instance)
|
|
82
|
+
expect(request).toHaveBeenCalledWith({
|
|
83
|
+
uri: '/projects/p',
|
|
84
|
+
query: {includeMembers: 'true', includeFeatures: 'true'},
|
|
85
|
+
tag: 'projects.get',
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('pre-seeds a memberless detail read from the cached default list without fetching', async () => {
|
|
90
|
+
const request = vi.fn().mockReturnValue(
|
|
91
|
+
of([
|
|
92
|
+
{id: 'p1', displayName: 'One'},
|
|
93
|
+
{id: 'p2', displayName: 'Two'},
|
|
94
|
+
]),
|
|
95
|
+
)
|
|
96
|
+
const mockClient = {
|
|
97
|
+
observable: {request} as unknown as SanityClient['observable'],
|
|
23
98
|
} as SanityClient
|
|
24
99
|
|
|
25
100
|
vi.mocked(getClientState).mockReturnValue({
|
|
26
101
|
observable: of(mockClient),
|
|
27
102
|
} as StateSource<SanityClient>)
|
|
28
103
|
|
|
29
|
-
|
|
30
|
-
expect(
|
|
31
|
-
|
|
104
|
+
await projects.resolveState(instance)
|
|
105
|
+
expect(request).toHaveBeenCalledTimes(1)
|
|
106
|
+
|
|
107
|
+
const source = project.getState(instance, {projectId: 'p1', includeMembers: false})
|
|
108
|
+
const unsubscribe = source.subscribe()
|
|
109
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
110
|
+
|
|
111
|
+
expect(source.getCurrent()).toMatchObject({status: 'success', data: {id: 'p1'}})
|
|
112
|
+
expect(request).toHaveBeenCalledTimes(1)
|
|
113
|
+
unsubscribe()
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('does not pre-seed a detail read that includes members', async () => {
|
|
117
|
+
const list = [{id: 'p1', displayName: 'One'}]
|
|
118
|
+
const detail = {id: 'p1', displayName: 'One', members: []}
|
|
119
|
+
const request = vi.fn().mockReturnValueOnce(of(list)).mockReturnValueOnce(of(detail))
|
|
120
|
+
const mockClient = {
|
|
121
|
+
observable: {request} as unknown as SanityClient['observable'],
|
|
122
|
+
} as SanityClient
|
|
123
|
+
|
|
124
|
+
vi.mocked(getClientState).mockReturnValue({
|
|
125
|
+
observable: of(mockClient),
|
|
126
|
+
} as StateSource<SanityClient>)
|
|
127
|
+
|
|
128
|
+
await projects.resolveState(instance)
|
|
129
|
+
|
|
130
|
+
const result = await project.resolveState(instance, {projectId: 'p1'})
|
|
131
|
+
expect(result).toEqual(detail)
|
|
132
|
+
expect(request).toHaveBeenCalledTimes(2)
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
describe('project cache key generation', () => {
|
|
137
|
+
const mockInstance = {config: {projectId: 'p'}} as SanityInstance
|
|
138
|
+
|
|
139
|
+
it('default call includes :members and :features (both default-true)', () => {
|
|
140
|
+
expect(getProjectCacheKey(mockInstance)).toBe('project:p:members:features')
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('treats undefined and the matching default as the same key', () => {
|
|
144
|
+
expect(getProjectCacheKey(mockInstance)).toBe(
|
|
145
|
+
getProjectCacheKey(mockInstance, {includeMembers: true, includeFeatures: true}),
|
|
146
|
+
)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('explicit includeFeatures: false drops the :features segment', () => {
|
|
150
|
+
expect(getProjectCacheKey(mockInstance, {includeFeatures: false})).toBe('project:p:members')
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('explicit includeMembers: false drops the :members segment', () => {
|
|
154
|
+
expect(getProjectCacheKey(mockInstance, {includeMembers: false})).toBe('project:p:features')
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('combines all segments in order', () => {
|
|
158
|
+
expect(
|
|
159
|
+
getProjectCacheKey(mockInstance, {
|
|
160
|
+
projectId: 'a',
|
|
161
|
+
includeMembers: true,
|
|
162
|
+
includeFeatures: true,
|
|
163
|
+
}),
|
|
164
|
+
).toBe('project:a:members:features')
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('produces distinct keys for each meaningful option permutation', () => {
|
|
168
|
+
const keys = new Set([
|
|
169
|
+
getProjectCacheKey(mockInstance),
|
|
170
|
+
getProjectCacheKey(mockInstance, {includeMembers: false}),
|
|
171
|
+
getProjectCacheKey(mockInstance, {includeFeatures: false}),
|
|
172
|
+
getProjectCacheKey(mockInstance, {includeMembers: false, includeFeatures: false}),
|
|
173
|
+
getProjectCacheKey(mockInstance, {projectId: 'a'}),
|
|
174
|
+
getProjectCacheKey(mockInstance, {projectId: 'b'}),
|
|
175
|
+
])
|
|
176
|
+
expect(keys.size).toBe(6)
|
|
32
177
|
})
|
|
33
178
|
})
|
package/src/project/project.ts
CHANGED
|
@@ -1,35 +1,148 @@
|
|
|
1
1
|
import {switchMap} from 'rxjs'
|
|
2
2
|
|
|
3
3
|
import {getClientState} from '../client/clientStore'
|
|
4
|
-
import {
|
|
4
|
+
import {type ProjectHandle} from '../config/sanityConfig'
|
|
5
|
+
import {projects} from '../projects/projects'
|
|
6
|
+
import {type SanityInstance} from '../store/createSanityInstance'
|
|
7
|
+
import {defineFetcher} from '../store/fetcherStore'
|
|
8
|
+
import {buildQuery} from '../utils/buildQuery'
|
|
5
9
|
|
|
6
10
|
const API_VERSION = 'v2025-02-19'
|
|
7
11
|
|
|
8
12
|
/** @public */
|
|
9
|
-
export
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
getKey: (_instance, options: ProjectHandle) => {
|
|
15
|
-
const projectId = options?.projectId
|
|
16
|
-
if (!projectId) {
|
|
17
|
-
throw new Error('A projectId is required to use the project API.')
|
|
18
|
-
}
|
|
19
|
-
return projectId
|
|
20
|
-
},
|
|
21
|
-
fetcher: (instance) => (options: ProjectHandle) => {
|
|
22
|
-
const projectId = options.projectId
|
|
23
|
-
|
|
24
|
-
return getClientState(instance, {
|
|
25
|
-
apiVersion: API_VERSION,
|
|
26
|
-
scope: 'global',
|
|
27
|
-
projectId,
|
|
28
|
-
}).observable.pipe(switchMap((client) => client.observable.projects.getById(projectId!)))
|
|
29
|
-
},
|
|
30
|
-
})
|
|
13
|
+
export interface ProjectMemberRole {
|
|
14
|
+
name: string
|
|
15
|
+
title: string
|
|
16
|
+
description: string
|
|
17
|
+
}
|
|
31
18
|
|
|
32
19
|
/** @public */
|
|
33
|
-
export
|
|
20
|
+
export interface ProjectMember {
|
|
21
|
+
id: string
|
|
22
|
+
createdAt: string
|
|
23
|
+
updatedAt: string
|
|
24
|
+
isCurrentUser: boolean
|
|
25
|
+
isRobot: boolean
|
|
26
|
+
roles: ProjectMemberRole[]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** @public */
|
|
30
|
+
export interface ProjectMetadata {
|
|
31
|
+
color?: string
|
|
32
|
+
externalStudioHost?: string
|
|
33
|
+
initialTemplate?: string
|
|
34
|
+
cliInitializedAt?: string
|
|
35
|
+
integration: 'manage' | 'cli'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The base fields returned from `/projects` for every project.
|
|
40
|
+
* @public
|
|
41
|
+
*/
|
|
42
|
+
export interface ProjectBase {
|
|
43
|
+
id: string
|
|
44
|
+
displayName: string
|
|
45
|
+
studioHost: string | null
|
|
46
|
+
organizationId: string
|
|
47
|
+
metadata: ProjectMetadata
|
|
48
|
+
isBlocked: boolean
|
|
49
|
+
isDisabled: boolean
|
|
50
|
+
isDisabledByUser: boolean
|
|
51
|
+
activityFeedEnabled: boolean
|
|
52
|
+
createdAt: string
|
|
53
|
+
updatedAt: string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A `Project` with `members` and/or `features` conditionally included
|
|
58
|
+
* based on the query options used to fetch it.
|
|
59
|
+
* @public
|
|
60
|
+
*/
|
|
61
|
+
export type Project<
|
|
62
|
+
IncludeMembers extends boolean = true,
|
|
63
|
+
IncludeFeatures extends boolean = true,
|
|
64
|
+
> = ProjectBase &
|
|
65
|
+
// `boolean extends T` is non-distributive — true only when T is the wide
|
|
66
|
+
// `boolean`, in which case the field is optional. Literal `true`/`false`
|
|
67
|
+
// fall through to the strict branch.
|
|
68
|
+
(boolean extends IncludeMembers
|
|
69
|
+
? {members?: ProjectMember[]}
|
|
70
|
+
: IncludeMembers extends true
|
|
71
|
+
? {members: ProjectMember[]}
|
|
72
|
+
: unknown) &
|
|
73
|
+
(boolean extends IncludeFeatures
|
|
74
|
+
? {features?: string[]}
|
|
75
|
+
: IncludeFeatures extends true
|
|
76
|
+
? {features: string[]}
|
|
77
|
+
: unknown)
|
|
78
|
+
|
|
34
79
|
/** @public */
|
|
35
|
-
export
|
|
80
|
+
export interface ProjectOptions<
|
|
81
|
+
IncludeMembers extends boolean = true,
|
|
82
|
+
IncludeFeatures extends boolean = true,
|
|
83
|
+
> extends ProjectHandle {
|
|
84
|
+
includeMembers?: IncludeMembers
|
|
85
|
+
includeFeatures?: IncludeFeatures
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function normalizeProjectOptions(options?: ProjectOptions<boolean, boolean>) {
|
|
89
|
+
return {
|
|
90
|
+
includeMembers: options?.includeMembers ?? true,
|
|
91
|
+
includeFeatures: options?.includeFeatures ?? true,
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function resolveProjectId(instance: SanityInstance, options?: ProjectOptions<boolean, boolean>) {
|
|
96
|
+
const projectId = options?.projectId ?? instance.config.projectId
|
|
97
|
+
if (!projectId) {
|
|
98
|
+
throw new Error('A projectId is required to use the project API.')
|
|
99
|
+
}
|
|
100
|
+
return projectId
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** @internal */
|
|
104
|
+
export function getProjectCacheKey(
|
|
105
|
+
instance: SanityInstance,
|
|
106
|
+
options?: ProjectOptions<boolean, boolean>,
|
|
107
|
+
): string {
|
|
108
|
+
const projectId = resolveProjectId(instance, options)
|
|
109
|
+
const {includeMembers, includeFeatures} = normalizeProjectOptions(options)
|
|
110
|
+
const membersKey = includeMembers ? ':members' : ''
|
|
111
|
+
const featuresKey = includeFeatures ? ':features' : ''
|
|
112
|
+
return `project:${projectId}${membersKey}${featuresKey}`
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Fetcher for a single project (`GET /projects/:id`), on the shared fetcher
|
|
117
|
+
* cache. Pre-seeds from the default {@link projects} list entry when it can
|
|
118
|
+
* satisfy the read — the default list carries features but not members, so
|
|
119
|
+
* only `includeMembers: false` reads seed.
|
|
120
|
+
*
|
|
121
|
+
* @internal
|
|
122
|
+
*/
|
|
123
|
+
export const project = defineFetcher<
|
|
124
|
+
[options?: ProjectOptions<boolean, boolean>],
|
|
125
|
+
Project<boolean, boolean>
|
|
126
|
+
>({
|
|
127
|
+
name: 'project',
|
|
128
|
+
getKey: getProjectCacheKey,
|
|
129
|
+
fetch: (instance) => (options) =>
|
|
130
|
+
getClientState(instance, {apiVersion: API_VERSION, scope: 'global'}).observable.pipe(
|
|
131
|
+
switchMap((client) =>
|
|
132
|
+
client.observable.request<Project<boolean, boolean>>({
|
|
133
|
+
uri: `/projects/${resolveProjectId(instance, options)}`,
|
|
134
|
+
query: buildQuery(normalizeProjectOptions(options)),
|
|
135
|
+
tag: 'projects.get',
|
|
136
|
+
}),
|
|
137
|
+
),
|
|
138
|
+
),
|
|
139
|
+
tags: (data) => [{type: 'project', id: data.id}],
|
|
140
|
+
initialData: (instance, options) => {
|
|
141
|
+
if (normalizeProjectOptions(options).includeMembers) return undefined
|
|
142
|
+
const snapshot = projects.getState(instance).getCurrent()
|
|
143
|
+
if (snapshot.status !== 'success') return undefined
|
|
144
|
+
const projectId = resolveProjectId(instance, options)
|
|
145
|
+
const match = snapshot.data.find((item) => item.id === projectId)
|
|
146
|
+
return match && {data: match, dataUpdatedAt: snapshot.dataUpdatedAt}
|
|
147
|
+
},
|
|
148
|
+
})
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import {NEVER} from 'rxjs'
|
|
2
2
|
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
|
|
3
3
|
|
|
4
|
-
import {type DocumentResource} from '../config/sanityConfig'
|
|
5
4
|
import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
|
|
6
5
|
import {type StoreState} from '../store/createStoreState'
|
|
7
6
|
import {hashString} from '../utils/hashString'
|
|
@@ -31,8 +30,7 @@ vi.mock('./subscribeToStateAndFetchBatches.ts')
|
|
|
31
30
|
|
|
32
31
|
describe('getProjectionState', () => {
|
|
33
32
|
let instance: SanityInstance
|
|
34
|
-
const
|
|
35
|
-
const docHandle = {documentId: 'exampleId', documentType: 'exampleType', resource}
|
|
33
|
+
const docHandle = {documentId: 'exampleId', documentType: 'exampleType'}
|
|
36
34
|
const projection1 = '{exampleProjection1}'
|
|
37
35
|
const hash1 = hashString(projection1)
|
|
38
36
|
const projection2 = '{exampleProjection2}'
|
|
@@ -50,7 +48,7 @@ describe('getProjectionState', () => {
|
|
|
50
48
|
return NEVER.subscribe()
|
|
51
49
|
})
|
|
52
50
|
|
|
53
|
-
instance = createSanityInstance()
|
|
51
|
+
instance = createSanityInstance({projectId: 'exampleProject', dataset: 'exampleDataset'})
|
|
54
52
|
vi.useFakeTimers() // Enable fake timers for each test
|
|
55
53
|
})
|
|
56
54
|
|
|
@@ -128,7 +126,7 @@ describe('getProjectionState', () => {
|
|
|
128
126
|
expect(state.get().subscriptions).toEqual({})
|
|
129
127
|
expect(state.get().documentProjections).toEqual({})
|
|
130
128
|
|
|
131
|
-
const unsubscribe1 = projectionState1.subscribe(vi.fn()) // Should use ID
|
|
129
|
+
const unsubscribe1 = projectionState1.subscribe(vi.fn()) // Should use ID 2
|
|
132
130
|
expect(state.get().subscriptions).toEqual({
|
|
133
131
|
[docHandle.documentId]: {[hash1]: {testSubId_2: true}},
|
|
134
132
|
})
|
|
@@ -136,7 +134,7 @@ describe('getProjectionState', () => {
|
|
|
136
134
|
[docHandle.documentId]: {[hash1]: projection1},
|
|
137
135
|
})
|
|
138
136
|
|
|
139
|
-
const unsubscribe2 = projectionState2.subscribe(vi.fn()) // Should use ID
|
|
137
|
+
const unsubscribe2 = projectionState2.subscribe(vi.fn()) // Should use ID 3
|
|
140
138
|
expect(state.get().subscriptions).toEqual({
|
|
141
139
|
[docHandle.documentId]: {
|
|
142
140
|
[hash1]: {testSubId_2: true},
|
|
@@ -150,10 +148,12 @@ describe('getProjectionState', () => {
|
|
|
150
148
|
},
|
|
151
149
|
})
|
|
152
150
|
|
|
153
|
-
|
|
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())
|
|
154
154
|
expect(state.get().subscriptions).toEqual({
|
|
155
155
|
[docHandle.documentId]: {
|
|
156
|
-
[hash1]: {testSubId_2: true
|
|
156
|
+
[hash1]: {testSubId_2: true},
|
|
157
157
|
[hash2]: {testSubId_3: true},
|
|
158
158
|
},
|
|
159
159
|
})
|
|
@@ -167,23 +167,20 @@ describe('getProjectionState', () => {
|
|
|
167
167
|
})
|
|
168
168
|
|
|
169
169
|
// --- Test Unsubscribe ---
|
|
170
|
-
unsubscribe1() // Unsubscribes
|
|
171
|
-
expect(state.get().subscriptions[docHandle.documentId]?.[hash1]).toEqual({
|
|
172
|
-
testSubId_2: true,
|
|
173
|
-
testSubId_4: true,
|
|
174
|
-
})
|
|
170
|
+
unsubscribe1() // Unsubscribes first subscription consumer
|
|
171
|
+
expect(state.get().subscriptions[docHandle.documentId]?.[hash1]).toEqual({testSubId_2: true})
|
|
175
172
|
vi.advanceTimersByTime(PROJECTION_STATE_CLEAR_DELAY)
|
|
176
|
-
expect(state.get().subscriptions[docHandle.documentId]?.[hash1]).toEqual({
|
|
173
|
+
expect(state.get().subscriptions[docHandle.documentId]?.[hash1]).toEqual({testSubId_2: true})
|
|
177
174
|
expect(state.get().documentProjections[docHandle.documentId]?.[hash1]).toEqual(projection1)
|
|
178
175
|
|
|
179
|
-
unsubscribe3() //
|
|
176
|
+
unsubscribe3() // Also unsubscibes first projection subscription, should be removed
|
|
180
177
|
vi.advanceTimersByTime(PROJECTION_STATE_CLEAR_DELAY)
|
|
181
178
|
expect(state.get().subscriptions[docHandle.documentId]?.[hash1]).toBeUndefined()
|
|
182
179
|
expect(state.get().documentProjections[docHandle.documentId]?.[hash1]).toBeUndefined()
|
|
183
180
|
expect(state.get().subscriptions[docHandle.documentId]?.[hash2]).toEqual({testSubId_3: true})
|
|
184
181
|
expect(state.get().documentProjections[docHandle.documentId]?.[hash2]).toEqual(projection2)
|
|
185
182
|
|
|
186
|
-
unsubscribe2() // Unsubscribes
|
|
183
|
+
unsubscribe2() // Unsubscribes second projection subscription, should be removed
|
|
187
184
|
vi.advanceTimersByTime(PROJECTION_STATE_CLEAR_DELAY)
|
|
188
185
|
expect(state.get().subscriptions[docHandle.documentId]).toBeUndefined()
|
|
189
186
|
expect(state.get().documentProjections[docHandle.documentId]).toBeUndefined()
|
|
@@ -205,14 +202,14 @@ describe('getProjectionState', () => {
|
|
|
205
202
|
}))
|
|
206
203
|
|
|
207
204
|
const unsubscribe1 = projectionState.subscribe(vi.fn()) // Should use ID 2
|
|
208
|
-
const unsubscribe2 = projectionState.subscribe(vi.fn()) // Should
|
|
205
|
+
const unsubscribe2 = projectionState.subscribe(vi.fn()) // Should reuse ID 2
|
|
209
206
|
|
|
210
207
|
expect(state.get().values[docHandle.documentId]?.[hash]).toEqual({
|
|
211
208
|
data: initialData,
|
|
212
209
|
isPending: true,
|
|
213
210
|
})
|
|
214
211
|
|
|
215
|
-
unsubscribe1() // Unsubscribes
|
|
212
|
+
unsubscribe1() // Unsubscribes first subscription consumer
|
|
216
213
|
vi.advanceTimersByTime(PROJECTION_STATE_CLEAR_DELAY)
|
|
217
214
|
expect(state.get().values[docHandle.documentId]?.[hash]).toEqual({
|
|
218
215
|
data: initialData,
|
|
@@ -221,9 +218,10 @@ describe('getProjectionState', () => {
|
|
|
221
218
|
expect(Object.keys(state.get().subscriptions[docHandle.documentId]?.[hash] ?? {}).length).toBe(
|
|
222
219
|
1,
|
|
223
220
|
)
|
|
224
|
-
|
|
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})
|
|
225
223
|
|
|
226
|
-
unsubscribe2() // Unsubscribes
|
|
224
|
+
unsubscribe2() // Unsubscribes second subscription consumer
|
|
227
225
|
expect(state.get().values[docHandle.documentId]?.[hash]).toEqual({
|
|
228
226
|
data: initialData,
|
|
229
227
|
isPending: true,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {DocumentId, getPublishedId} from '@sanity/id-utils'
|
|
2
|
-
import {
|
|
2
|
+
import {type SanityProjectionResult} from 'groq'
|
|
3
3
|
|
|
4
4
|
import {type DocumentHandle} from '../config/sanityConfig'
|
|
5
5
|
import {bindActionByResourceAndPerspective} from '../store/createActionBinder'
|
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
} from '../store/createStateSourceAction'
|
|
12
12
|
import {hashString} from '../utils/hashString'
|
|
13
13
|
import {insecureRandomId} from '../utils/ids'
|
|
14
|
+
import {omitProperty} from '../utils/object'
|
|
15
|
+
import {setCleanupTimeout} from '../utils/setCleanupTimeout'
|
|
14
16
|
import {projectionStore} from './projectionStore'
|
|
15
17
|
import {type ProjectionStoreState, type ProjectionValuePending} from './types'
|
|
16
18
|
import {PROJECTION_STATE_CLEAR_DELAY, STABLE_EMPTY_PROJECTION, validateProjection} from './util'
|
|
@@ -24,6 +26,24 @@ export interface ProjectionOptions<
|
|
|
24
26
|
projection: TProjection
|
|
25
27
|
}
|
|
26
28
|
|
|
29
|
+
/**
|
|
30
|
+
* @beta
|
|
31
|
+
*/
|
|
32
|
+
export function getProjectionState<
|
|
33
|
+
TProjection extends string = string,
|
|
34
|
+
TDocumentType extends string = string,
|
|
35
|
+
TDataset extends string = string,
|
|
36
|
+
TProjectId extends string = string,
|
|
37
|
+
>(
|
|
38
|
+
instance: SanityInstance,
|
|
39
|
+
options: ProjectionOptions<TProjection, TDocumentType, TDataset, TProjectId>,
|
|
40
|
+
): StateSource<
|
|
41
|
+
| ProjectionValuePending<
|
|
42
|
+
SanityProjectionResult<TProjection, TDocumentType, `${TProjectId}.${TDataset}`>
|
|
43
|
+
>
|
|
44
|
+
| undefined
|
|
45
|
+
>
|
|
46
|
+
|
|
27
47
|
/**
|
|
28
48
|
* @beta
|
|
29
49
|
*/
|
|
@@ -91,9 +111,9 @@ export const _getProjectionState = bindActionByResourceAndPerspective(
|
|
|
91
111
|
}))
|
|
92
112
|
|
|
93
113
|
return () => {
|
|
94
|
-
|
|
114
|
+
setCleanupTimeout(() => {
|
|
95
115
|
state.set('removeSubscription', (prev): Partial<ProjectionStoreState> => {
|
|
96
|
-
const documentSubscriptionsForHash =
|
|
116
|
+
const documentSubscriptionsForHash = omitProperty(
|
|
97
117
|
prev.subscriptions[documentId]?.[projectionHash],
|
|
98
118
|
subscriptionId,
|
|
99
119
|
)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {type ClientPerspective} from '@sanity/client'
|
|
2
|
+
import {DocumentId, getPublishedId} from '@sanity/id-utils'
|
|
2
3
|
|
|
3
|
-
import {type
|
|
4
|
-
import {getPublishedId} from '../utils/ids'
|
|
4
|
+
import {type ReleasePerspective} from '../config/sanityConfig'
|
|
5
5
|
import {
|
|
6
6
|
type DocumentProjections,
|
|
7
7
|
type DocumentProjectionValues,
|
|
@@ -67,7 +67,7 @@ interface ProcessProjectionQueryOptions {
|
|
|
67
67
|
ids: Set<string>
|
|
68
68
|
results: ProjectionQueryResult[]
|
|
69
69
|
documentStatuses?: ProjectionStoreState['documentStatuses']
|
|
70
|
-
perspective:
|
|
70
|
+
perspective: ClientPerspective | ReleasePerspective
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
export function processProjectionQuery({
|
|
@@ -84,7 +84,7 @@ export function processProjectionQuery({
|
|
|
84
84
|
} = {}
|
|
85
85
|
|
|
86
86
|
for (const result of results) {
|
|
87
|
-
const originalId = getPublishedId(result._id)
|
|
87
|
+
const originalId = getPublishedId(DocumentId(result._id))
|
|
88
88
|
const hash = result.__projectionHash
|
|
89
89
|
|
|
90
90
|
if (!ids.has(originalId)) continue
|
|
@@ -24,7 +24,7 @@ describe('projectionStore', () => {
|
|
|
24
24
|
new Observable(subscriber).subscribe(),
|
|
25
25
|
)
|
|
26
26
|
|
|
27
|
-
const instance = createSanityInstance()
|
|
27
|
+
const instance = createSanityInstance({projectId: 'p', dataset: 'd'})
|
|
28
28
|
|
|
29
29
|
const {state, dispose} = createStoreInstance(
|
|
30
30
|
instance,
|
|
@@ -23,7 +23,7 @@ describe('resolveProjection', () => {
|
|
|
23
23
|
} as ProjectionValuePending<Record<string, unknown>>),
|
|
24
24
|
} as StateSource<ProjectionValuePending<Record<string, unknown>>>)
|
|
25
25
|
|
|
26
|
-
instance = createSanityInstance()
|
|
26
|
+
instance = createSanityInstance({projectId: 'p', dataset: 'd'})
|
|
27
27
|
})
|
|
28
28
|
|
|
29
29
|
afterEach(() => {
|
|
@@ -34,7 +34,6 @@ describe('resolveProjection', () => {
|
|
|
34
34
|
const docHandle = createDocumentHandle({
|
|
35
35
|
documentId: 'doc123',
|
|
36
36
|
documentType: 'movie',
|
|
37
|
-
resource: {projectId: 'p', dataset: 'd'},
|
|
38
37
|
})
|
|
39
38
|
const projection = '{title}'
|
|
40
39
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {type SanityProjectionResult} from 'groq'
|
|
1
2
|
import {filter, firstValueFrom} from 'rxjs'
|
|
2
3
|
|
|
3
4
|
import {bindActionByResourceAndPerspective} from '../store/createActionBinder'
|
|
@@ -6,6 +7,21 @@ import {getProjectionState, type ProjectionOptions} from './getProjectionState'
|
|
|
6
7
|
import {projectionStore} from './projectionStore'
|
|
7
8
|
import {type ProjectionValuePending} from './types'
|
|
8
9
|
|
|
10
|
+
/** @beta */
|
|
11
|
+
export function resolveProjection<
|
|
12
|
+
TProjection extends string = string,
|
|
13
|
+
TDocumentType extends string = string,
|
|
14
|
+
TDataset extends string = string,
|
|
15
|
+
TProjectId extends string = string,
|
|
16
|
+
>(
|
|
17
|
+
instance: SanityInstance,
|
|
18
|
+
options: ProjectionOptions<TProjection, TDocumentType, TDataset, TProjectId>,
|
|
19
|
+
): Promise<
|
|
20
|
+
ProjectionValuePending<
|
|
21
|
+
SanityProjectionResult<TProjection, TDocumentType, `${TProjectId}.${TDataset}`>
|
|
22
|
+
>
|
|
23
|
+
>
|
|
24
|
+
|
|
9
25
|
/** @beta */
|
|
10
26
|
export function resolveProjection<TData extends object>(
|
|
11
27
|
instance: SanityInstance,
|
|
@@ -23,7 +23,7 @@ describe('subscribeToStateAndFetchBatches', () => {
|
|
|
23
23
|
|
|
24
24
|
beforeEach(() => {
|
|
25
25
|
vi.clearAllMocks()
|
|
26
|
-
instance = createSanityInstance()
|
|
26
|
+
instance = createSanityInstance({projectId: 'test', dataset: 'test'})
|
|
27
27
|
state = createStoreState<ProjectionStoreState>({
|
|
28
28
|
documentProjections: {},
|
|
29
29
|
documentStatuses: {},
|