@sanity/sdk 2.3.1 → 2.5.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 +173 -105
- package/dist/index.js +354 -122
- package/dist/index.js.map +1 -1
- package/package.json +12 -11
- package/src/_exports/index.ts +30 -0
- package/src/agent/agentActions.test.ts +81 -0
- package/src/agent/agentActions.ts +139 -0
- package/src/auth/authStore.test.ts +13 -13
- package/src/auth/refreshStampedToken.test.ts +16 -16
- package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +6 -6
- package/src/auth/subscribeToStorageEventsAndSetToken.test.ts +4 -4
- package/src/auth/utils.ts +36 -0
- package/src/client/clientStore.test.ts +151 -0
- package/src/client/clientStore.ts +39 -1
- package/src/comlink/controller/actions/destroyController.test.ts +2 -2
- package/src/comlink/controller/actions/getOrCreateChannel.test.ts +6 -6
- package/src/comlink/controller/actions/getOrCreateController.test.ts +5 -5
- package/src/comlink/controller/actions/getOrCreateController.ts +1 -1
- package/src/comlink/controller/actions/releaseChannel.test.ts +3 -2
- package/src/comlink/controller/comlinkControllerStore.test.ts +4 -4
- package/src/comlink/node/actions/getOrCreateNode.test.ts +7 -7
- package/src/comlink/node/actions/releaseNode.test.ts +2 -2
- package/src/comlink/node/comlinkNodeStore.test.ts +4 -3
- package/src/config/sanityConfig.ts +49 -3
- package/src/document/actions.test.ts +34 -0
- package/src/document/actions.ts +31 -7
- package/src/document/applyDocumentActions.test.ts +9 -6
- package/src/document/applyDocumentActions.ts +9 -49
- package/src/document/documentStore.test.ts +148 -107
- package/src/document/documentStore.ts +40 -10
- package/src/document/permissions.test.ts +9 -9
- package/src/document/permissions.ts +17 -7
- package/src/document/processActions.test.ts +345 -0
- package/src/document/processActions.ts +185 -2
- package/src/document/reducers.ts +13 -6
- package/src/presence/presenceStore.ts +13 -7
- package/src/preview/previewStore.test.ts +10 -2
- package/src/preview/previewStore.ts +2 -1
- package/src/preview/subscribeToStateAndFetchBatches.test.ts +8 -5
- package/src/preview/subscribeToStateAndFetchBatches.ts +9 -3
- package/src/projection/projectionStore.test.ts +18 -2
- package/src/projection/projectionStore.ts +2 -1
- package/src/projection/subscribeToStateAndFetchBatches.test.ts +6 -5
- package/src/projection/subscribeToStateAndFetchBatches.ts +9 -3
- package/src/query/queryStore.ts +7 -4
- package/src/releases/getPerspectiveState.ts +2 -2
- package/src/releases/releasesStore.ts +10 -4
- package/src/store/createActionBinder.test.ts +8 -6
- package/src/store/createActionBinder.ts +50 -14
- package/src/store/createStateSourceAction.test.ts +12 -11
- package/src/store/createStateSourceAction.ts +6 -6
- package/src/store/createStoreInstance.test.ts +29 -16
- package/src/store/createStoreInstance.ts +6 -5
- package/src/store/defineStore.test.ts +1 -1
- package/src/store/defineStore.ts +12 -7
package/src/auth/utils.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {type ClientError} from '@sanity/client'
|
|
1
2
|
import {EMPTY, fromEvent, Observable} from 'rxjs'
|
|
2
3
|
|
|
3
4
|
import {AUTH_CODE_PARAM, DEFAULT_BASE} from './authConstants'
|
|
@@ -134,3 +135,38 @@ export function getCleanedUrl(locationUrl: string): string {
|
|
|
134
135
|
loc.searchParams.delete('url')
|
|
135
136
|
return loc.toString()
|
|
136
137
|
}
|
|
138
|
+
|
|
139
|
+
// -----------------------------------------------------------------------------
|
|
140
|
+
// ClientError helpers (shared)
|
|
141
|
+
// -----------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
/** @internal */
|
|
144
|
+
export type ApiErrorBody = {
|
|
145
|
+
error?: {type?: string; description?: string}
|
|
146
|
+
type?: string
|
|
147
|
+
description?: string
|
|
148
|
+
message?: string
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** @internal Extracts the structured API error body from a ClientError, if present. */
|
|
152
|
+
export function getClientErrorApiBody(error: ClientError): ApiErrorBody | undefined {
|
|
153
|
+
const body: unknown = (error as ClientError).response?.body
|
|
154
|
+
return body && typeof body === 'object' ? (body as ApiErrorBody) : undefined
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** @internal Returns the error type string from an API error body, if available. */
|
|
158
|
+
export function getClientErrorApiType(error: ClientError): string | undefined {
|
|
159
|
+
const body = getClientErrorApiBody(error)
|
|
160
|
+
return body?.error?.type ?? body?.type
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** @internal Returns the error description string from an API error body, if available. */
|
|
164
|
+
export function getClientErrorApiDescription(error: ClientError): string | undefined {
|
|
165
|
+
const body = getClientErrorApiBody(error)
|
|
166
|
+
return body?.error?.description ?? body?.description
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** @internal True if the error represents a projectUserNotFoundError. */
|
|
170
|
+
export function isProjectUserNotFoundClientError(error: ClientError): boolean {
|
|
171
|
+
return getClientErrorApiType(error) === 'projectUserNotFoundError'
|
|
172
|
+
}
|
|
@@ -3,6 +3,7 @@ import {Subject} from 'rxjs'
|
|
|
3
3
|
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
4
4
|
|
|
5
5
|
import {getAuthMethodState, getTokenState} from '../auth/authStore'
|
|
6
|
+
import {canvasSource, datasetSource, mediaLibrarySource} from '../config/sanityConfig'
|
|
6
7
|
import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
|
|
7
8
|
import {getClient, getClientState} from './clientStore'
|
|
8
9
|
|
|
@@ -75,6 +76,20 @@ describe('clientStore', () => {
|
|
|
75
76
|
).toThrowError(/unsupported properties: illegalKey/)
|
|
76
77
|
})
|
|
77
78
|
|
|
79
|
+
it('should throw a helpful error when called without options', () => {
|
|
80
|
+
expect(() =>
|
|
81
|
+
// @ts-expect-error Testing missing options
|
|
82
|
+
getClient(instance, undefined),
|
|
83
|
+
).toThrowError(/requires a configuration object with at least an "apiVersion" property/)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('should throw a helpful error when called with null options', () => {
|
|
87
|
+
expect(() =>
|
|
88
|
+
// @ts-expect-error Testing null options
|
|
89
|
+
getClient(instance, null),
|
|
90
|
+
).toThrowError(/requires a configuration object with at least an "apiVersion" property/)
|
|
91
|
+
})
|
|
92
|
+
|
|
78
93
|
it('should reuse clients with identical configurations', () => {
|
|
79
94
|
const options = {apiVersion: '2024-11-12', useCdn: true}
|
|
80
95
|
const client1 = getClient(instance, options)
|
|
@@ -158,4 +173,140 @@ describe('clientStore', () => {
|
|
|
158
173
|
subscription.unsubscribe()
|
|
159
174
|
})
|
|
160
175
|
})
|
|
176
|
+
|
|
177
|
+
describe('source handling', () => {
|
|
178
|
+
it('should create client when source is provided', () => {
|
|
179
|
+
const source = datasetSource('source-project', 'source-dataset')
|
|
180
|
+
const client = getClient(instance, {apiVersion: '2024-11-12', source})
|
|
181
|
+
|
|
182
|
+
expect(vi.mocked(createClient)).toHaveBeenCalledWith(
|
|
183
|
+
expect.objectContaining({
|
|
184
|
+
apiVersion: '2024-11-12',
|
|
185
|
+
source: expect.objectContaining({
|
|
186
|
+
__sanity_internal_sourceId: {
|
|
187
|
+
projectId: 'source-project',
|
|
188
|
+
dataset: 'source-dataset',
|
|
189
|
+
},
|
|
190
|
+
}),
|
|
191
|
+
}),
|
|
192
|
+
)
|
|
193
|
+
// Client should be projectless - no projectId/dataset in config
|
|
194
|
+
expect(client.config()).not.toHaveProperty('projectId')
|
|
195
|
+
expect(client.config()).not.toHaveProperty('dataset')
|
|
196
|
+
expect(client.config()).toEqual(
|
|
197
|
+
expect.objectContaining({
|
|
198
|
+
source: expect.objectContaining({
|
|
199
|
+
__sanity_internal_sourceId: {
|
|
200
|
+
projectId: 'source-project',
|
|
201
|
+
dataset: 'source-dataset',
|
|
202
|
+
},
|
|
203
|
+
}),
|
|
204
|
+
}),
|
|
205
|
+
)
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('should create resource when source has array sourceId and be projectless', () => {
|
|
209
|
+
const source = mediaLibrarySource('media-lib-123')
|
|
210
|
+
const client = getClient(instance, {apiVersion: '2024-11-12', source})
|
|
211
|
+
|
|
212
|
+
expect(vi.mocked(createClient)).toHaveBeenCalledWith(
|
|
213
|
+
expect.objectContaining({
|
|
214
|
+
'~experimental_resource': {type: 'media-library', id: 'media-lib-123'},
|
|
215
|
+
'apiVersion': '2024-11-12',
|
|
216
|
+
}),
|
|
217
|
+
)
|
|
218
|
+
// Client should be projectless - no projectId/dataset in config
|
|
219
|
+
expect(client.config()).not.toHaveProperty('projectId')
|
|
220
|
+
expect(client.config()).not.toHaveProperty('dataset')
|
|
221
|
+
expect(client.config()).toEqual(
|
|
222
|
+
expect.objectContaining({
|
|
223
|
+
'~experimental_resource': {type: 'media-library', id: 'media-lib-123'},
|
|
224
|
+
}),
|
|
225
|
+
)
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
it('should create resource when canvas source is provided and be projectless', () => {
|
|
229
|
+
const source = canvasSource('canvas-123')
|
|
230
|
+
const client = getClient(instance, {apiVersion: '2024-11-12', source})
|
|
231
|
+
|
|
232
|
+
expect(vi.mocked(createClient)).toHaveBeenCalledWith(
|
|
233
|
+
expect.objectContaining({
|
|
234
|
+
'~experimental_resource': {type: 'canvas', id: 'canvas-123'},
|
|
235
|
+
'apiVersion': '2024-11-12',
|
|
236
|
+
}),
|
|
237
|
+
)
|
|
238
|
+
// Client should be projectless - no projectId/dataset in config
|
|
239
|
+
expect(client.config()).not.toHaveProperty('projectId')
|
|
240
|
+
expect(client.config()).not.toHaveProperty('dataset')
|
|
241
|
+
expect(client.config()).toEqual(
|
|
242
|
+
expect.objectContaining({
|
|
243
|
+
'~experimental_resource': {type: 'canvas', id: 'canvas-123'},
|
|
244
|
+
}),
|
|
245
|
+
)
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
it('should create projectless client when source is provided, ignoring instance config', () => {
|
|
249
|
+
const source = datasetSource('source-project', 'source-dataset')
|
|
250
|
+
const client = getClient(instance, {apiVersion: '2024-11-12', source})
|
|
251
|
+
|
|
252
|
+
// Client should be projectless - source takes precedence, instance config is ignored
|
|
253
|
+
expect(client.config()).not.toHaveProperty('projectId')
|
|
254
|
+
expect(client.config()).not.toHaveProperty('dataset')
|
|
255
|
+
expect(client.config()).toEqual(
|
|
256
|
+
expect.objectContaining({
|
|
257
|
+
source: expect.objectContaining({
|
|
258
|
+
__sanity_internal_sourceId: {
|
|
259
|
+
projectId: 'source-project',
|
|
260
|
+
dataset: 'source-dataset',
|
|
261
|
+
},
|
|
262
|
+
}),
|
|
263
|
+
}),
|
|
264
|
+
)
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
it('should warn when both source and explicit projectId/dataset are provided', () => {
|
|
268
|
+
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
269
|
+
const source = datasetSource('source-project', 'source-dataset')
|
|
270
|
+
const client = getClient(instance, {
|
|
271
|
+
apiVersion: '2024-11-12',
|
|
272
|
+
source,
|
|
273
|
+
projectId: 'explicit-project',
|
|
274
|
+
dataset: 'explicit-dataset',
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
278
|
+
'Both source and explicit projectId/dataset are provided. The source will be used and projectId/dataset will be ignored.',
|
|
279
|
+
)
|
|
280
|
+
// Client should still be projectless despite explicit projectId/dataset
|
|
281
|
+
expect(client.config()).not.toHaveProperty('projectId')
|
|
282
|
+
expect(client.config()).not.toHaveProperty('dataset')
|
|
283
|
+
consoleSpy.mockRestore()
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
it('should create different clients for different sources', () => {
|
|
287
|
+
const source1 = datasetSource('project-1', 'dataset-1')
|
|
288
|
+
const source2 = datasetSource('project-2', 'dataset-2')
|
|
289
|
+
const source3 = mediaLibrarySource('media-lib-1')
|
|
290
|
+
|
|
291
|
+
const client1 = getClient(instance, {apiVersion: '2024-11-12', source: source1})
|
|
292
|
+
const client2 = getClient(instance, {apiVersion: '2024-11-12', source: source2})
|
|
293
|
+
const client3 = getClient(instance, {apiVersion: '2024-11-12', source: source3})
|
|
294
|
+
|
|
295
|
+
expect(client1).not.toBe(client2)
|
|
296
|
+
expect(client2).not.toBe(client3)
|
|
297
|
+
expect(client1).not.toBe(client3)
|
|
298
|
+
expect(vi.mocked(createClient)).toHaveBeenCalledTimes(3)
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
it('should reuse clients with identical source configurations', () => {
|
|
302
|
+
const source = datasetSource('same-project', 'same-dataset')
|
|
303
|
+
const options = {apiVersion: '2024-11-12', source}
|
|
304
|
+
|
|
305
|
+
const client1 = getClient(instance, options)
|
|
306
|
+
const client2 = getClient(instance, options)
|
|
307
|
+
|
|
308
|
+
expect(client1).toBe(client2)
|
|
309
|
+
expect(vi.mocked(createClient)).toHaveBeenCalledTimes(1)
|
|
310
|
+
})
|
|
311
|
+
})
|
|
161
312
|
})
|
|
@@ -2,6 +2,7 @@ import {type ClientConfig, createClient, type SanityClient} from '@sanity/client
|
|
|
2
2
|
import {pick} from 'lodash-es'
|
|
3
3
|
|
|
4
4
|
import {getAuthMethodState, getTokenState} from '../auth/authStore'
|
|
5
|
+
import {type DocumentSource, SOURCE_ID} from '../config/sanityConfig'
|
|
5
6
|
import {bindActionGlobally} from '../store/createActionBinder'
|
|
6
7
|
import {createStateSourceAction} from '../store/createStateSourceAction'
|
|
7
8
|
import {defineStore, type StoreContext} from '../store/defineStore'
|
|
@@ -39,6 +40,7 @@ const allowedKeys = Object.keys({
|
|
|
39
40
|
'requestTagPrefix': null,
|
|
40
41
|
'useProjectHostname': null,
|
|
41
42
|
'~experimental_resource': null,
|
|
43
|
+
'source': null,
|
|
42
44
|
} satisfies Record<keyof ClientOptions, null>) as (keyof ClientOptions)[]
|
|
43
45
|
|
|
44
46
|
const DEFAULT_CLIENT_CONFIG: ClientConfig = {
|
|
@@ -90,6 +92,11 @@ export interface ClientOptions extends Pick<ClientConfig, AllowedClientConfigKey
|
|
|
90
92
|
* @internal
|
|
91
93
|
*/
|
|
92
94
|
'~experimental_resource'?: ClientConfig['~experimental_resource']
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @internal
|
|
98
|
+
*/
|
|
99
|
+
'source'?: DocumentSource
|
|
93
100
|
}
|
|
94
101
|
|
|
95
102
|
const clientStore = defineStore<ClientStoreState>({
|
|
@@ -142,6 +149,13 @@ const getClientConfigKey = (options: ClientOptions) => JSON.stringify(pick(optio
|
|
|
142
149
|
export const getClient = bindActionGlobally(
|
|
143
150
|
clientStore,
|
|
144
151
|
({state, instance}, options: ClientOptions) => {
|
|
152
|
+
if (!options || typeof options !== 'object') {
|
|
153
|
+
throw new Error(
|
|
154
|
+
'getClient() requires a configuration object with at least an "apiVersion" property. ' +
|
|
155
|
+
'Example: getClient(instance, { apiVersion: "2024-11-12" })',
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
145
159
|
// Check for disallowed keys
|
|
146
160
|
const providedKeys = Object.keys(options) as (keyof ClientOptions)[]
|
|
147
161
|
const disallowedKeys = providedKeys.filter((key) => !allowedKeys.includes(key))
|
|
@@ -156,18 +170,42 @@ export const getClient = bindActionGlobally(
|
|
|
156
170
|
|
|
157
171
|
const tokenFromState = state.get().token
|
|
158
172
|
const {clients, authMethod} = state.get()
|
|
173
|
+
const hasSource = !!options.source
|
|
174
|
+
let sourceId = options.source?.[SOURCE_ID]
|
|
175
|
+
|
|
176
|
+
let resource
|
|
177
|
+
if (Array.isArray(sourceId)) {
|
|
178
|
+
resource = {type: sourceId[0], id: sourceId[1]}
|
|
179
|
+
sourceId = undefined
|
|
180
|
+
}
|
|
181
|
+
|
|
159
182
|
const projectId = options.projectId ?? instance.config.projectId
|
|
160
183
|
const dataset = options.dataset ?? instance.config.dataset
|
|
161
184
|
const apiHost = options.apiHost ?? instance.config.auth?.apiHost
|
|
162
185
|
|
|
163
186
|
const effectiveOptions: ClientOptions = {
|
|
164
187
|
...DEFAULT_CLIENT_CONFIG,
|
|
165
|
-
...((options.scope === 'global' || !projectId) && {useProjectHostname: false}),
|
|
188
|
+
...((options.scope === 'global' || !projectId || hasSource) && {useProjectHostname: false}),
|
|
166
189
|
token: authMethod === 'cookie' ? undefined : (tokenFromState ?? undefined),
|
|
167
190
|
...options,
|
|
168
191
|
...(projectId && {projectId}),
|
|
169
192
|
...(dataset && {dataset}),
|
|
170
193
|
...(apiHost && {apiHost}),
|
|
194
|
+
...(resource && {'~experimental_resource': resource}),
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// When a source is provided, don't use projectId/dataset - the client should be "projectless"
|
|
198
|
+
// The client code itself will ignore the non-source config, so we do this to prevent confusing the user.
|
|
199
|
+
// (ref: https://github.com/sanity-io/client/blob/5c23f81f5ab93a53f5b22b39845c867988508d84/src/data/dataMethods.ts#L691)
|
|
200
|
+
if (hasSource) {
|
|
201
|
+
if (options.projectId || options.dataset) {
|
|
202
|
+
// eslint-disable-next-line no-console
|
|
203
|
+
console.warn(
|
|
204
|
+
'Both source and explicit projectId/dataset are provided. The source will be used and projectId/dataset will be ignored.',
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
delete effectiveOptions.projectId
|
|
208
|
+
delete effectiveOptions.dataset
|
|
171
209
|
}
|
|
172
210
|
|
|
173
211
|
if (effectiveOptions.token === null || typeof effectiveOptions.token === 'undefined') {
|
|
@@ -39,7 +39,7 @@ describe('destroyController', () => {
|
|
|
39
39
|
})
|
|
40
40
|
|
|
41
41
|
// Execute action
|
|
42
|
-
destroyController({state, instance})
|
|
42
|
+
destroyController({state, instance, key: null})
|
|
43
43
|
|
|
44
44
|
// Verify controller was destroyed and state was cleared
|
|
45
45
|
expect(mockController.destroy).toHaveBeenCalled()
|
|
@@ -49,7 +49,7 @@ describe('destroyController', () => {
|
|
|
49
49
|
|
|
50
50
|
it('should do nothing if no controller exists', () => {
|
|
51
51
|
// State already has null controller, so just execute action
|
|
52
|
-
expect(() => destroyController({state, instance})).not.toThrow()
|
|
52
|
+
expect(() => destroyController({state, instance, key: null})).not.toThrow()
|
|
53
53
|
|
|
54
54
|
// State should remain unchanged
|
|
55
55
|
expect(state.get().controller).toBeNull()
|
|
@@ -49,7 +49,7 @@ describe('getOrCreateChannel', () => {
|
|
|
49
49
|
it('should create a new channel using the controller', () => {
|
|
50
50
|
const createChannelSpy = vi.spyOn(mockController, 'createChannel')
|
|
51
51
|
|
|
52
|
-
const channel = getOrCreateChannel({state, instance}, channelConfig)
|
|
52
|
+
const channel = getOrCreateChannel({state, instance, key: null}, channelConfig)
|
|
53
53
|
|
|
54
54
|
expect(createChannelSpy).toHaveBeenCalledWith(channelConfig)
|
|
55
55
|
expect(channel.on).toBeDefined()
|
|
@@ -70,17 +70,17 @@ describe('getOrCreateChannel', () => {
|
|
|
70
70
|
channels: new Map(),
|
|
71
71
|
})
|
|
72
72
|
|
|
73
|
-
expect(() => getOrCreateChannel({state, instance}, channelConfig)).toThrow(
|
|
73
|
+
expect(() => getOrCreateChannel({state, instance, key: null}, channelConfig)).toThrow(
|
|
74
74
|
'Controller must be initialized before using or creating channels',
|
|
75
75
|
)
|
|
76
76
|
})
|
|
77
77
|
|
|
78
78
|
it('should retrieve channel directly from store once created', () => {
|
|
79
|
-
const createdChannel = getOrCreateChannel({state, instance}, channelConfig)
|
|
79
|
+
const createdChannel = getOrCreateChannel({state, instance, key: null}, channelConfig)
|
|
80
80
|
vi.clearAllMocks() // Clear call counts
|
|
81
81
|
|
|
82
82
|
// Retrieve channel again
|
|
83
|
-
const retrievedChannel = getOrCreateChannel({state, instance}, channelConfig)
|
|
83
|
+
const retrievedChannel = getOrCreateChannel({state, instance, key: null}, channelConfig)
|
|
84
84
|
expect(retrievedChannel).toBeDefined()
|
|
85
85
|
expect(retrievedChannel).toBe(createdChannel)
|
|
86
86
|
|
|
@@ -95,10 +95,10 @@ describe('getOrCreateChannel', () => {
|
|
|
95
95
|
})
|
|
96
96
|
|
|
97
97
|
it('should throw error when trying to create channel with different options', () => {
|
|
98
|
-
getOrCreateChannel({state, instance}, channelConfig)
|
|
98
|
+
getOrCreateChannel({state, instance, key: null}, channelConfig)
|
|
99
99
|
|
|
100
100
|
expect(() =>
|
|
101
|
-
getOrCreateChannel({state, instance}, {...channelConfig, connectTo: 'window'}),
|
|
101
|
+
getOrCreateChannel({state, instance, key: null}, {...channelConfig, connectTo: 'window'}),
|
|
102
102
|
).toThrow('Channel "test" already exists with different options')
|
|
103
103
|
})
|
|
104
104
|
})
|
|
@@ -43,7 +43,7 @@ describe('getOrCreateController', () => {
|
|
|
43
43
|
const controllerSpy = vi.spyOn(comlink, 'createController')
|
|
44
44
|
const targetOrigin = 'https://test.sanity.dev'
|
|
45
45
|
|
|
46
|
-
const controller = getOrCreateController({state, instance}, targetOrigin)
|
|
46
|
+
const controller = getOrCreateController({state, instance, key: null}, targetOrigin)
|
|
47
47
|
|
|
48
48
|
expect(controllerSpy).toHaveBeenCalledWith({targetOrigin})
|
|
49
49
|
expect(controller).toBeDefined()
|
|
@@ -56,8 +56,8 @@ describe('getOrCreateController', () => {
|
|
|
56
56
|
const controllerSpy = vi.spyOn(comlink, 'createController')
|
|
57
57
|
const targetOrigin = 'https://test.sanity.dev'
|
|
58
58
|
|
|
59
|
-
const firstController = getOrCreateController({state, instance}, targetOrigin)
|
|
60
|
-
const secondController = getOrCreateController({state, instance}, targetOrigin)
|
|
59
|
+
const firstController = getOrCreateController({state, instance, key: null}, targetOrigin)
|
|
60
|
+
const secondController = getOrCreateController({state, instance, key: null}, targetOrigin)
|
|
61
61
|
|
|
62
62
|
expect(controllerSpy).toHaveBeenCalledTimes(1)
|
|
63
63
|
expect(firstController).toBe(secondController)
|
|
@@ -68,9 +68,9 @@ describe('getOrCreateController', () => {
|
|
|
68
68
|
const targetOrigin = 'https://test.sanity.dev'
|
|
69
69
|
const targetOrigin2 = 'https://test2.sanity.dev'
|
|
70
70
|
|
|
71
|
-
const firstController = getOrCreateController({state, instance}, targetOrigin)
|
|
71
|
+
const firstController = getOrCreateController({state, instance, key: null}, targetOrigin)
|
|
72
72
|
const destroySpy = vi.spyOn(firstController, 'destroy')
|
|
73
|
-
const secondController = getOrCreateController({state, instance}, targetOrigin2)
|
|
73
|
+
const secondController = getOrCreateController({state, instance, key: null}, targetOrigin2)
|
|
74
74
|
|
|
75
75
|
expect(controllerSpy).toHaveBeenCalledTimes(2)
|
|
76
76
|
expect(destroySpy).toHaveBeenCalled()
|
|
@@ -21,7 +21,7 @@ export const getOrCreateController = (
|
|
|
21
21
|
// if the target origin has changed, we'll create a new controller,
|
|
22
22
|
// but need to clean up first
|
|
23
23
|
if (controller) {
|
|
24
|
-
destroyController({state, instance})
|
|
24
|
+
destroyController({state, instance, key: undefined})
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
const newController = createController({targetOrigin})
|
|
@@ -19,6 +19,7 @@ const channelConfig = {
|
|
|
19
19
|
describe('releaseChannel', () => {
|
|
20
20
|
let instance: SanityInstance
|
|
21
21
|
let store: StoreInstance<ComlinkControllerState>
|
|
22
|
+
const key = {name: 'global', projectId: 'test-project-id', dataset: 'test-dataset'}
|
|
22
23
|
|
|
23
24
|
let getOrCreateChannel: (
|
|
24
25
|
inst: SanityInstance,
|
|
@@ -29,14 +30,14 @@ describe('releaseChannel', () => {
|
|
|
29
30
|
|
|
30
31
|
beforeEach(() => {
|
|
31
32
|
instance = createSanityInstance({projectId: 'test-project-id', dataset: 'test-dataset'})
|
|
32
|
-
store = createStoreInstance(instance, comlinkControllerStore)
|
|
33
|
+
store = createStoreInstance(instance, key, comlinkControllerStore)
|
|
33
34
|
|
|
34
35
|
const bind =
|
|
35
36
|
<TParams extends unknown[], TReturn>(
|
|
36
37
|
action: StoreAction<ComlinkControllerState, TParams, TReturn>,
|
|
37
38
|
) =>
|
|
38
39
|
(inst: SanityInstance, ...params: TParams) =>
|
|
39
|
-
action({instance: inst, state: store.state}, ...params)
|
|
40
|
+
action({instance: inst, state: store.state, key}, ...params)
|
|
40
41
|
|
|
41
42
|
getOrCreateChannel = bind(unboundGetOrCreateChannel)
|
|
42
43
|
getOrCreateController = bind(unboundGetOrCreateController)
|
|
@@ -31,7 +31,7 @@ describe('comlinkControllerStore', () => {
|
|
|
31
31
|
|
|
32
32
|
// Create store state directly
|
|
33
33
|
const state = createStoreState<ComlinkControllerState>(
|
|
34
|
-
comlinkControllerStore.getInitialState(instance),
|
|
34
|
+
comlinkControllerStore.getInitialState(instance, null),
|
|
35
35
|
)
|
|
36
36
|
|
|
37
37
|
const initialState = state.get()
|
|
@@ -56,13 +56,13 @@ describe('comlinkControllerStore', () => {
|
|
|
56
56
|
vi.mocked(bindActionGlobally).mockImplementation(
|
|
57
57
|
(_storeDef, action) =>
|
|
58
58
|
(inst: SanityInstance, ...params: unknown[]) =>
|
|
59
|
-
action({instance: inst, state}, ...params),
|
|
59
|
+
action({instance: inst, state, key: {name: 'global'}}, ...params),
|
|
60
60
|
)
|
|
61
61
|
|
|
62
62
|
const {comlinkControllerStore} = await import('./comlinkControllerStore')
|
|
63
63
|
|
|
64
64
|
// Get the cleanup function from the store
|
|
65
|
-
const dispose = comlinkControllerStore.initialize?.({state, instance})
|
|
65
|
+
const dispose = comlinkControllerStore.initialize?.({state, instance, key: null})
|
|
66
66
|
|
|
67
67
|
// Run cleanup
|
|
68
68
|
dispose?.()
|
|
@@ -82,7 +82,7 @@ describe('comlinkControllerStore', () => {
|
|
|
82
82
|
})
|
|
83
83
|
|
|
84
84
|
// Get the cleanup function
|
|
85
|
-
const cleanup = comlinkControllerStore.initialize?.({state, instance})
|
|
85
|
+
const cleanup = comlinkControllerStore.initialize?.({state, instance, key: null})
|
|
86
86
|
|
|
87
87
|
// Should not throw when no controller exists
|
|
88
88
|
expect(() => cleanup?.()).not.toThrow()
|
|
@@ -40,24 +40,24 @@ describe('getOrCreateNode', () => {
|
|
|
40
40
|
})
|
|
41
41
|
|
|
42
42
|
it('should create and start a node', () => {
|
|
43
|
-
const node = getOrCreateNode({state, instance}, nodeConfig)
|
|
43
|
+
const node = getOrCreateNode({state, instance, key: null}, nodeConfig)
|
|
44
44
|
|
|
45
45
|
expect(comlink.createNode).toHaveBeenCalledWith(nodeConfig)
|
|
46
46
|
expect(node.start).toHaveBeenCalled()
|
|
47
47
|
})
|
|
48
48
|
|
|
49
49
|
it('should store the node in nodeStore', () => {
|
|
50
|
-
const node = getOrCreateNode({state, instance}, nodeConfig)
|
|
50
|
+
const node = getOrCreateNode({state, instance, key: null}, nodeConfig)
|
|
51
51
|
|
|
52
|
-
expect(getOrCreateNode({state, instance}, nodeConfig)).toBe(node)
|
|
52
|
+
expect(getOrCreateNode({state, instance, key: null}, nodeConfig)).toBe(node)
|
|
53
53
|
})
|
|
54
54
|
|
|
55
55
|
it('should throw error when trying to create node with different options', () => {
|
|
56
|
-
getOrCreateNode({state, instance}, nodeConfig)
|
|
56
|
+
getOrCreateNode({state, instance, key: null}, nodeConfig)
|
|
57
57
|
|
|
58
58
|
expect(() =>
|
|
59
59
|
getOrCreateNode(
|
|
60
|
-
{state, instance},
|
|
60
|
+
{state, instance, key: null},
|
|
61
61
|
{
|
|
62
62
|
...nodeConfig,
|
|
63
63
|
connectTo: 'window',
|
|
@@ -74,7 +74,7 @@ describe('getOrCreateNode', () => {
|
|
|
74
74
|
return statusUnsubMock
|
|
75
75
|
})
|
|
76
76
|
|
|
77
|
-
getOrCreateNode({state, instance}, nodeConfig)
|
|
77
|
+
getOrCreateNode({state, instance, key: null}, nodeConfig)
|
|
78
78
|
|
|
79
79
|
expect(mockNode.onStatus).toHaveBeenCalled()
|
|
80
80
|
expect(state.get().nodes.get(nodeConfig.name)?.statusUnsub).toBe(statusUnsubMock)
|
|
@@ -92,7 +92,7 @@ describe('getOrCreateNode', () => {
|
|
|
92
92
|
return statusUnsubMock
|
|
93
93
|
})
|
|
94
94
|
|
|
95
|
-
getOrCreateNode({state, instance}, nodeConfig)
|
|
95
|
+
getOrCreateNode({state, instance, key: null}, nodeConfig)
|
|
96
96
|
|
|
97
97
|
// Remove the node entry before triggering the status callback
|
|
98
98
|
state.get().nodes.delete(nodeConfig.name)
|
|
@@ -47,7 +47,7 @@ describe('releaseNode', () => {
|
|
|
47
47
|
expect(state.get().nodes.has('test-node')).toBe(true)
|
|
48
48
|
|
|
49
49
|
// Release the node
|
|
50
|
-
releaseNode({state, instance}, 'test-node')
|
|
50
|
+
releaseNode({state, instance, key: null}, 'test-node')
|
|
51
51
|
|
|
52
52
|
// Check node is removed
|
|
53
53
|
expect(mockNode.stop).toHaveBeenCalled()
|
|
@@ -64,7 +64,7 @@ describe('releaseNode', () => {
|
|
|
64
64
|
})
|
|
65
65
|
state.set('setup', {nodes})
|
|
66
66
|
|
|
67
|
-
releaseNode({state, instance}, 'test-node')
|
|
67
|
+
releaseNode({state, instance, key: null}, 'test-node')
|
|
68
68
|
|
|
69
69
|
expect(statusUnsub).toHaveBeenCalled()
|
|
70
70
|
})
|
|
@@ -14,7 +14,7 @@ describe('nodeStore', () => {
|
|
|
14
14
|
})
|
|
15
15
|
|
|
16
16
|
it('should have correct initial state', () => {
|
|
17
|
-
const initialState = comlinkNodeStore.getInitialState(instance)
|
|
17
|
+
const initialState = comlinkNodeStore.getInitialState(instance, null)
|
|
18
18
|
|
|
19
19
|
expect(initialState.nodes).toBeInstanceOf(Map)
|
|
20
20
|
expect(initialState.nodes.size).toBe(0)
|
|
@@ -25,16 +25,17 @@ describe('nodeStore', () => {
|
|
|
25
25
|
stop: vi.fn(),
|
|
26
26
|
} as unknown as Node<WindowMessage, FrameMessage>
|
|
27
27
|
|
|
28
|
-
const initialState = comlinkNodeStore.getInitialState(instance)
|
|
28
|
+
const initialState = comlinkNodeStore.getInitialState(instance, null)
|
|
29
29
|
initialState.nodes.set('test-node', {
|
|
30
30
|
options: {name: 'test-node', connectTo: 'parent'},
|
|
31
31
|
node: mockNode,
|
|
32
|
-
|
|
32
|
+
status: 'idle',
|
|
33
33
|
})
|
|
34
34
|
|
|
35
35
|
const cleanup = comlinkNodeStore.initialize?.({
|
|
36
36
|
instance,
|
|
37
37
|
state: createStoreState(initialState),
|
|
38
|
+
key: null,
|
|
38
39
|
})
|
|
39
40
|
|
|
40
41
|
cleanup?.()
|
|
@@ -29,15 +29,14 @@ export interface PerspectiveHandle {
|
|
|
29
29
|
* @public
|
|
30
30
|
*/
|
|
31
31
|
export interface DatasetHandle<TDataset extends string = string, TProjectId extends string = string>
|
|
32
|
-
extends ProjectHandle<TProjectId>,
|
|
33
|
-
PerspectiveHandle {
|
|
32
|
+
extends ProjectHandle<TProjectId>, PerspectiveHandle {
|
|
34
33
|
dataset?: TDataset
|
|
35
34
|
}
|
|
36
35
|
|
|
37
36
|
/**
|
|
38
37
|
* Identifies a specific document type within a Sanity dataset and project.
|
|
39
38
|
* Includes `projectId`, `dataset`, and `documentType`.
|
|
40
|
-
* Optionally includes a `documentId
|
|
39
|
+
* Optionally includes a `documentId` and `liveEdit` flag.
|
|
41
40
|
* @public
|
|
42
41
|
*/
|
|
43
42
|
export interface DocumentTypeHandle<
|
|
@@ -47,6 +46,12 @@ export interface DocumentTypeHandle<
|
|
|
47
46
|
> extends DatasetHandle<TDataset, TProjectId> {
|
|
48
47
|
documentId?: string
|
|
49
48
|
documentType: TDocumentType
|
|
49
|
+
/**
|
|
50
|
+
* Indicates whether this document uses liveEdit mode.
|
|
51
|
+
* When `true`, the document does not use the draft/published model and edits are applied directly to the document.
|
|
52
|
+
* @see https://www.sanity.io/docs/content-lake/drafts#ca0663a8f002
|
|
53
|
+
*/
|
|
54
|
+
liveEdit?: boolean
|
|
50
55
|
}
|
|
51
56
|
|
|
52
57
|
/**
|
|
@@ -81,3 +86,44 @@ export interface SanityConfig extends DatasetHandle, PerspectiveHandle {
|
|
|
81
86
|
enabled: boolean
|
|
82
87
|
}
|
|
83
88
|
}
|
|
89
|
+
|
|
90
|
+
export const SOURCE_ID = '__sanity_internal_sourceId'
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* A document source can be used for querying.
|
|
94
|
+
*
|
|
95
|
+
* @beta
|
|
96
|
+
* @see datasetSource Construct a document source for a given projectId and dataset.
|
|
97
|
+
* @see mediaLibrarySource Construct a document source for a mediaLibraryId.
|
|
98
|
+
* @see canvasSource Construct a document source for a canvasId.
|
|
99
|
+
*/
|
|
100
|
+
export type DocumentSource = {
|
|
101
|
+
[SOURCE_ID]: ['media-library', string] | ['canvas', string] | {projectId: string; dataset: string}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Returns a document source for a projectId and dataset.
|
|
106
|
+
*
|
|
107
|
+
* @beta
|
|
108
|
+
*/
|
|
109
|
+
export function datasetSource(projectId: string, dataset: string): DocumentSource {
|
|
110
|
+
return {[SOURCE_ID]: {projectId, dataset}}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Returns a document source for a Media Library.
|
|
115
|
+
*
|
|
116
|
+
* @beta
|
|
117
|
+
*/
|
|
118
|
+
export function mediaLibrarySource(id: string): DocumentSource {
|
|
119
|
+
return {[SOURCE_ID]: ['media-library', id]}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Returns a document source for a Canvas.
|
|
124
|
+
*
|
|
125
|
+
* @beta
|
|
126
|
+
*/
|
|
127
|
+
export function canvasSource(id: string): DocumentSource {
|
|
128
|
+
return {[SOURCE_ID]: ['canvas', id]}
|
|
129
|
+
}
|
|
@@ -42,6 +42,40 @@ describe('document actions', () => {
|
|
|
42
42
|
documentType: typeHandle.documentType,
|
|
43
43
|
})
|
|
44
44
|
})
|
|
45
|
+
|
|
46
|
+
it('creates a document action with initial values', () => {
|
|
47
|
+
const initialValue = {
|
|
48
|
+
title: 'Test Title',
|
|
49
|
+
author: 'John Doe',
|
|
50
|
+
count: 42,
|
|
51
|
+
}
|
|
52
|
+
const action = createDocument(dummyDocHandle, initialValue)
|
|
53
|
+
expect(action).toEqual({
|
|
54
|
+
type: 'document.create',
|
|
55
|
+
documentId: 'abc123',
|
|
56
|
+
documentType: dummyDocHandle.documentType,
|
|
57
|
+
initialValue,
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('creates a document action without initialValue when not provided', () => {
|
|
62
|
+
const action = createDocument(dummyDocHandle, undefined)
|
|
63
|
+
expect(action).toEqual({
|
|
64
|
+
type: 'document.create',
|
|
65
|
+
documentId: 'abc123',
|
|
66
|
+
documentType: dummyDocHandle.documentType,
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('creates a document action with empty initialValue object', () => {
|
|
71
|
+
const action = createDocument(dummyDocHandle, {})
|
|
72
|
+
expect(action).toEqual({
|
|
73
|
+
type: 'document.create',
|
|
74
|
+
documentId: 'abc123',
|
|
75
|
+
documentType: dummyDocHandle.documentType,
|
|
76
|
+
initialValue: {},
|
|
77
|
+
})
|
|
78
|
+
})
|
|
45
79
|
})
|
|
46
80
|
|
|
47
81
|
describe('deleteDocument', () => {
|