@sanity/sdk 2.3.0 → 2.4.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 +144 -17
- package/dist/index.js +171 -46
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
- 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 +12 -20
- package/src/auth/authStore.ts +46 -15
- package/src/auth/studioModeAuth.test.ts +4 -4
- package/src/auth/studioModeAuth.ts +4 -5
- package/src/auth/subscribeToStateAndFetchCurrentUser.ts +20 -9
- package/src/auth/utils.ts +36 -0
- package/src/client/clientStore.test.ts +151 -0
- package/src/client/clientStore.ts +39 -1
- package/src/config/sanityConfig.ts +41 -0
- package/src/document/actions.test.ts +34 -0
- package/src/document/actions.ts +20 -0
- package/src/document/documentStore.test.ts +28 -0
- package/src/document/processActions.test.ts +97 -0
- package/src/document/processActions.ts +12 -2
- package/src/query/queryStore.ts +7 -4
- package/src/store/createActionBinder.ts +27 -6
|
@@ -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') {
|
|
@@ -81,3 +81,44 @@ export interface SanityConfig extends DatasetHandle, PerspectiveHandle {
|
|
|
81
81
|
enabled: boolean
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
|
+
|
|
85
|
+
export const SOURCE_ID = '__sanity_internal_sourceId'
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* A document source can be used for querying.
|
|
89
|
+
*
|
|
90
|
+
* @beta
|
|
91
|
+
* @see datasetSource Construct a document source for a given projectId and dataset.
|
|
92
|
+
* @see mediaLibrarySource Construct a document source for a mediaLibraryId.
|
|
93
|
+
* @see canvasSource Construct a document source for a canvasId.
|
|
94
|
+
*/
|
|
95
|
+
export type DocumentSource = {
|
|
96
|
+
[SOURCE_ID]: ['media-library', string] | ['canvas', string] | {projectId: string; dataset: string}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Returns a document source for a projectId and dataset.
|
|
101
|
+
*
|
|
102
|
+
* @beta
|
|
103
|
+
*/
|
|
104
|
+
export function datasetSource(projectId: string, dataset: string): DocumentSource {
|
|
105
|
+
return {[SOURCE_ID]: {projectId, dataset}}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Returns a document source for a Media Library.
|
|
110
|
+
*
|
|
111
|
+
* @beta
|
|
112
|
+
*/
|
|
113
|
+
export function mediaLibrarySource(id: string): DocumentSource {
|
|
114
|
+
return {[SOURCE_ID]: ['media-library', id]}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Returns a document source for a Canvas.
|
|
119
|
+
*
|
|
120
|
+
* @beta
|
|
121
|
+
*/
|
|
122
|
+
export function canvasSource(id: string): DocumentSource {
|
|
123
|
+
return {[SOURCE_ID]: ['canvas', id]}
|
|
124
|
+
}
|
|
@@ -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', () => {
|
package/src/document/actions.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {SanityEncoder} from '@sanity/mutate'
|
|
2
2
|
import {type PatchMutation as SanityMutatePatchMutation} from '@sanity/mutate/_unstable_store'
|
|
3
3
|
import {type PatchMutation, type PatchOperations} from '@sanity/types'
|
|
4
|
+
import {type SanityDocument} from 'groq'
|
|
4
5
|
|
|
5
6
|
import {type DocumentHandle, type DocumentTypeHandle} from '../config/sanityConfig'
|
|
6
7
|
import {getPublishedId} from '../utils/ids'
|
|
@@ -24,6 +25,17 @@ export interface CreateDocumentAction<
|
|
|
24
25
|
TProjectId extends string = string,
|
|
25
26
|
> extends DocumentTypeHandle<TDocumentType, TDataset, TProjectId> {
|
|
26
27
|
type: 'document.create'
|
|
28
|
+
/**
|
|
29
|
+
* Optional initial field values for the document.
|
|
30
|
+
* These values will be set when the document is created.
|
|
31
|
+
* System fields (_id, _type, _rev, _createdAt, _updatedAt) are omitted as they are set automatically.
|
|
32
|
+
*/
|
|
33
|
+
initialValue?: Partial<
|
|
34
|
+
Omit<
|
|
35
|
+
SanityDocument<TDocumentType, `${TProjectId}.${TDataset}`>,
|
|
36
|
+
'_id' | '_type' | '_rev' | '_createdAt' | '_updatedAt'
|
|
37
|
+
>
|
|
38
|
+
>
|
|
27
39
|
}
|
|
28
40
|
|
|
29
41
|
/**
|
|
@@ -111,6 +123,7 @@ export type DocumentAction<
|
|
|
111
123
|
/**
|
|
112
124
|
* Creates a `CreateDocumentAction` object.
|
|
113
125
|
* @param doc - A handle identifying the document type, dataset, and project. An optional `documentId` can be provided.
|
|
126
|
+
* @param initialValue - Optional initial field values for the document. (System fields are omitted as they are set automatically.)
|
|
114
127
|
* @returns A `CreateDocumentAction` object ready for dispatch.
|
|
115
128
|
* @beta
|
|
116
129
|
*/
|
|
@@ -120,11 +133,18 @@ export function createDocument<
|
|
|
120
133
|
TProjectId extends string = string,
|
|
121
134
|
>(
|
|
122
135
|
doc: DocumentTypeHandle<TDocumentType, TDataset, TProjectId>,
|
|
136
|
+
initialValue?: Partial<
|
|
137
|
+
Omit<
|
|
138
|
+
SanityDocument<TDocumentType, `${TProjectId}.${TDataset}`>,
|
|
139
|
+
'_id' | '_type' | '_rev' | '_createdAt' | '_updatedAt'
|
|
140
|
+
>
|
|
141
|
+
>,
|
|
123
142
|
): CreateDocumentAction<TDocumentType, TDataset, TProjectId> {
|
|
124
143
|
return {
|
|
125
144
|
type: 'document.create',
|
|
126
145
|
...doc,
|
|
127
146
|
...(doc.documentId && {documentId: getPublishedId(doc.documentId)}),
|
|
147
|
+
...(initialValue && {initialValue}),
|
|
128
148
|
}
|
|
129
149
|
}
|
|
130
150
|
|
|
@@ -110,6 +110,34 @@ it('creates, edits, and publishes a document', async () => {
|
|
|
110
110
|
unsubscribe()
|
|
111
111
|
})
|
|
112
112
|
|
|
113
|
+
it('creates a document with initial values', async () => {
|
|
114
|
+
const doc = createDocumentHandle({documentId: 'doc-with-initial', documentType: 'article'})
|
|
115
|
+
const documentState = getDocumentState(instance, doc)
|
|
116
|
+
|
|
117
|
+
expect(documentState.getCurrent()).toBeUndefined()
|
|
118
|
+
|
|
119
|
+
const unsubscribe = documentState.subscribe()
|
|
120
|
+
|
|
121
|
+
// Create a new document with initial field values
|
|
122
|
+
const {appeared} = await applyDocumentActions(
|
|
123
|
+
instance,
|
|
124
|
+
createDocument(doc, {
|
|
125
|
+
title: 'Article with Initial Values',
|
|
126
|
+
author: 'Jane Doe',
|
|
127
|
+
count: 42,
|
|
128
|
+
}),
|
|
129
|
+
)
|
|
130
|
+
expect(appeared).toContain(getDraftId(doc.documentId))
|
|
131
|
+
|
|
132
|
+
const currentDoc = documentState.getCurrent()
|
|
133
|
+
expect(currentDoc?._id).toEqual(getDraftId(doc.documentId))
|
|
134
|
+
expect(currentDoc?.title).toEqual('Article with Initial Values')
|
|
135
|
+
expect(currentDoc?.['author']).toEqual('Jane Doe')
|
|
136
|
+
expect(currentDoc?.['count']).toEqual(42)
|
|
137
|
+
|
|
138
|
+
unsubscribe()
|
|
139
|
+
})
|
|
140
|
+
|
|
113
141
|
it('edits existing documents', async () => {
|
|
114
142
|
const doc = createDocumentHandle({documentId: 'existing-doc', documentType: 'article'})
|
|
115
143
|
const state = getDocumentState(instance, doc)
|
|
@@ -138,6 +138,103 @@ describe('processActions', () => {
|
|
|
138
138
|
processActions({actions, transactionId, base, working, timestamp, grants}),
|
|
139
139
|
).toThrow(/You do not have permission to create a draft for document "doc1"/)
|
|
140
140
|
})
|
|
141
|
+
|
|
142
|
+
it('should create a draft document with initial values', () => {
|
|
143
|
+
const base: DocumentSet = {}
|
|
144
|
+
const working: DocumentSet = {}
|
|
145
|
+
const actions: DocumentAction[] = [
|
|
146
|
+
{
|
|
147
|
+
documentId: 'doc1',
|
|
148
|
+
type: 'document.create',
|
|
149
|
+
documentType: 'article',
|
|
150
|
+
initialValue: {
|
|
151
|
+
title: 'New Article',
|
|
152
|
+
author: 'John Doe',
|
|
153
|
+
count: 42,
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
]
|
|
157
|
+
const result = processActions({
|
|
158
|
+
actions,
|
|
159
|
+
transactionId,
|
|
160
|
+
base,
|
|
161
|
+
working,
|
|
162
|
+
timestamp,
|
|
163
|
+
grants: defaultGrants,
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
const draftId = 'drafts.doc1'
|
|
167
|
+
const draftDoc = result.working[draftId]
|
|
168
|
+
expect(draftDoc).toBeDefined()
|
|
169
|
+
expect(draftDoc?._id).toBe(draftId)
|
|
170
|
+
expect(draftDoc?._type).toBe('article')
|
|
171
|
+
expect(draftDoc?._rev).toBe(transactionId)
|
|
172
|
+
// Should have the initial values:
|
|
173
|
+
expect(draftDoc?.['title']).toBe('New Article')
|
|
174
|
+
expect(draftDoc?.['author']).toBe('John Doe')
|
|
175
|
+
expect(draftDoc?.['count']).toBe(42)
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('should create a draft with initial values that override published document values', () => {
|
|
179
|
+
const published = createDoc('doc1', 'Original Title')
|
|
180
|
+
const base: DocumentSet = {doc1: published}
|
|
181
|
+
const working: DocumentSet = {doc1: published}
|
|
182
|
+
const actions: DocumentAction[] = [
|
|
183
|
+
{
|
|
184
|
+
documentId: 'doc1',
|
|
185
|
+
type: 'document.create',
|
|
186
|
+
documentType: 'article',
|
|
187
|
+
initialValue: {
|
|
188
|
+
title: 'Overridden Title',
|
|
189
|
+
newField: 'New Value',
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
]
|
|
193
|
+
const result = processActions({
|
|
194
|
+
actions,
|
|
195
|
+
transactionId,
|
|
196
|
+
base,
|
|
197
|
+
working,
|
|
198
|
+
timestamp,
|
|
199
|
+
grants: defaultGrants,
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
const draftId = 'drafts.doc1'
|
|
203
|
+
const draftDoc = result.working[draftId]
|
|
204
|
+
expect(draftDoc).toBeDefined()
|
|
205
|
+
// Should have the overridden title from initialValue:
|
|
206
|
+
expect(draftDoc?.['title']).toBe('Overridden Title')
|
|
207
|
+
// Should have the new field:
|
|
208
|
+
expect(draftDoc?.['newField']).toBe('New Value')
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('should create a draft with empty initial values object', () => {
|
|
212
|
+
const base: DocumentSet = {}
|
|
213
|
+
const working: DocumentSet = {}
|
|
214
|
+
const actions: DocumentAction[] = [
|
|
215
|
+
{
|
|
216
|
+
documentId: 'doc1',
|
|
217
|
+
type: 'document.create',
|
|
218
|
+
documentType: 'article',
|
|
219
|
+
initialValue: {},
|
|
220
|
+
},
|
|
221
|
+
]
|
|
222
|
+
const result = processActions({
|
|
223
|
+
actions,
|
|
224
|
+
transactionId,
|
|
225
|
+
base,
|
|
226
|
+
working,
|
|
227
|
+
timestamp,
|
|
228
|
+
grants: defaultGrants,
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
const draftId = 'drafts.doc1'
|
|
232
|
+
const draftDoc = result.working[draftId]
|
|
233
|
+
expect(draftDoc).toBeDefined()
|
|
234
|
+
expect(draftDoc?._id).toBe(draftId)
|
|
235
|
+
expect(draftDoc?._type).toBe('article')
|
|
236
|
+
expect(draftDoc?._rev).toBe(transactionId)
|
|
237
|
+
})
|
|
141
238
|
})
|
|
142
239
|
|
|
143
240
|
describe('document.delete', () => {
|
|
@@ -152,8 +152,18 @@ export function processActions({
|
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
// Spread the (possibly undefined) published version directly.
|
|
155
|
-
const newDocBase = {
|
|
156
|
-
|
|
155
|
+
const newDocBase = {
|
|
156
|
+
...base[publishedId],
|
|
157
|
+
_type: action.documentType,
|
|
158
|
+
_id: draftId,
|
|
159
|
+
...action.initialValue,
|
|
160
|
+
}
|
|
161
|
+
const newDocWorking = {
|
|
162
|
+
...working[publishedId],
|
|
163
|
+
_type: action.documentType,
|
|
164
|
+
_id: draftId,
|
|
165
|
+
...action.initialValue,
|
|
166
|
+
}
|
|
157
167
|
const mutations: Mutation[] = [{create: newDocWorking}]
|
|
158
168
|
|
|
159
169
|
base = processMutations({
|
package/src/query/queryStore.ts
CHANGED
|
@@ -23,9 +23,9 @@ import {
|
|
|
23
23
|
} from 'rxjs'
|
|
24
24
|
|
|
25
25
|
import {getClientState} from '../client/clientStore'
|
|
26
|
-
import {type DatasetHandle} from '../config/sanityConfig'
|
|
26
|
+
import {type DatasetHandle, type DocumentSource} from '../config/sanityConfig'
|
|
27
27
|
import {getPerspectiveState} from '../releases/getPerspectiveState'
|
|
28
|
-
import {
|
|
28
|
+
import {bindActionBySource} from '../store/createActionBinder'
|
|
29
29
|
import {type SanityInstance} from '../store/createSanityInstance'
|
|
30
30
|
import {
|
|
31
31
|
createStateSourceAction,
|
|
@@ -62,6 +62,7 @@ export interface QueryOptions<
|
|
|
62
62
|
DatasetHandle<TDataset, TProjectId> {
|
|
63
63
|
query: TQuery
|
|
64
64
|
params?: Record<string, unknown>
|
|
65
|
+
source?: DocumentSource
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
/**
|
|
@@ -160,6 +161,7 @@ const listenForNewSubscribersAndFetch = ({state, instance}: StoreContext<QuerySt
|
|
|
160
161
|
projectId,
|
|
161
162
|
dataset,
|
|
162
163
|
tag,
|
|
164
|
+
source,
|
|
163
165
|
perspective: perspectiveFromOptions,
|
|
164
166
|
...restOptions
|
|
165
167
|
} = parseQueryKey(group$.key)
|
|
@@ -172,6 +174,7 @@ const listenForNewSubscribersAndFetch = ({state, instance}: StoreContext<QuerySt
|
|
|
172
174
|
apiVersion: QUERY_STORE_API_VERSION,
|
|
173
175
|
projectId,
|
|
174
176
|
dataset,
|
|
177
|
+
source,
|
|
175
178
|
}).observable
|
|
176
179
|
|
|
177
180
|
return combineLatest([lastLiveEventId$, client$, perspective$]).pipe(
|
|
@@ -290,7 +293,7 @@ export function getQueryState(
|
|
|
290
293
|
): ReturnType<typeof _getQueryState> {
|
|
291
294
|
return _getQueryState(...args)
|
|
292
295
|
}
|
|
293
|
-
const _getQueryState =
|
|
296
|
+
const _getQueryState = bindActionBySource(
|
|
294
297
|
queryStore,
|
|
295
298
|
createStateSourceAction({
|
|
296
299
|
selector: ({state, instance}: SelectorContext<QueryStoreState>, options: QueryOptions) => {
|
|
@@ -349,7 +352,7 @@ export function resolveQuery<TData>(
|
|
|
349
352
|
export function resolveQuery(...args: Parameters<typeof _resolveQuery>): Promise<unknown> {
|
|
350
353
|
return _resolveQuery(...args)
|
|
351
354
|
}
|
|
352
|
-
const _resolveQuery =
|
|
355
|
+
const _resolveQuery = bindActionBySource(
|
|
353
356
|
queryStore,
|
|
354
357
|
({state, instance}, {signal, ...options}: ResolveQueryOptions) => {
|
|
355
358
|
const normalized = normalizeOptionsWithPerspective(instance, options)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {type SanityConfig} from '../config/sanityConfig'
|
|
1
|
+
import {type DocumentSource, type SanityConfig, SOURCE_ID} from '../config/sanityConfig'
|
|
2
2
|
import {type SanityInstance} from './createSanityInstance'
|
|
3
3
|
import {createStoreInstance, type StoreInstance} from './createStoreInstance'
|
|
4
4
|
import {type StoreState} from './createStoreState'
|
|
@@ -43,7 +43,9 @@ export type BoundStoreAction<_TState, TParams extends unknown[], TReturn> = (
|
|
|
43
43
|
* )
|
|
44
44
|
* ```
|
|
45
45
|
*/
|
|
46
|
-
export function createActionBinder
|
|
46
|
+
export function createActionBinder<TKeyParams extends unknown[]>(
|
|
47
|
+
keyFn: (config: SanityConfig, ...params: TKeyParams) => string,
|
|
48
|
+
) {
|
|
47
49
|
const instanceRegistry = new Map<string, Set<string>>()
|
|
48
50
|
const storeRegistry = new Map<string, StoreInstance<unknown>>()
|
|
49
51
|
|
|
@@ -54,12 +56,12 @@ export function createActionBinder(keyFn: (config: SanityConfig) => string) {
|
|
|
54
56
|
* @param action - The action to bind
|
|
55
57
|
* @returns A function that executes the action with a Sanity instance
|
|
56
58
|
*/
|
|
57
|
-
return function bindAction<TState, TParams extends
|
|
59
|
+
return function bindAction<TState, TParams extends TKeyParams, TReturn>(
|
|
58
60
|
storeDefinition: StoreDefinition<TState>,
|
|
59
61
|
action: StoreAction<TState, TParams, TReturn>,
|
|
60
62
|
): BoundStoreAction<TState, TParams, TReturn> {
|
|
61
63
|
return function boundAction(instance: SanityInstance, ...params: TParams) {
|
|
62
|
-
const keySuffix = keyFn(instance.config)
|
|
64
|
+
const keySuffix = keyFn(instance.config, ...params)
|
|
63
65
|
const compositeKey = storeDefinition.name + (keySuffix ? `:${keySuffix}` : '')
|
|
64
66
|
|
|
65
67
|
// Get or create instance set for this composite key
|
|
@@ -128,13 +130,32 @@ export function createActionBinder(keyFn: (config: SanityConfig) => string) {
|
|
|
128
130
|
* fetchDocument(sanityInstance, 'doc123')
|
|
129
131
|
* ```
|
|
130
132
|
*/
|
|
131
|
-
export const bindActionByDataset = createActionBinder(({projectId, dataset}) => {
|
|
133
|
+
export const bindActionByDataset = createActionBinder<unknown[]>(({projectId, dataset}) => {
|
|
132
134
|
if (!projectId || !dataset) {
|
|
133
135
|
throw new Error('This API requires a project ID and dataset configured.')
|
|
134
136
|
}
|
|
135
137
|
return `${projectId}.${dataset}`
|
|
136
138
|
})
|
|
137
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Binds an action to a store that's scoped to a specific document source.
|
|
142
|
+
**/
|
|
143
|
+
export const bindActionBySource = createActionBinder<[{source?: DocumentSource}, ...unknown[]]>(
|
|
144
|
+
({projectId, dataset}, {source}) => {
|
|
145
|
+
if (source) {
|
|
146
|
+
const id = source[SOURCE_ID]
|
|
147
|
+
if (!id) throw new Error('Invalid source (missing ID information)')
|
|
148
|
+
if (Array.isArray(id)) return id.join(':')
|
|
149
|
+
return `${id.projectId}.${id.dataset}`
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!projectId || !dataset) {
|
|
153
|
+
throw new Error('This API requires a project ID and dataset configured.')
|
|
154
|
+
}
|
|
155
|
+
return `${projectId}.${dataset}`
|
|
156
|
+
},
|
|
157
|
+
)
|
|
158
|
+
|
|
138
159
|
/**
|
|
139
160
|
* Binds an action to a global store that's shared across all Sanity instances
|
|
140
161
|
*
|
|
@@ -173,4 +194,4 @@ export const bindActionByDataset = createActionBinder(({projectId, dataset}) =>
|
|
|
173
194
|
* getCurrentUser(sanityInstance)
|
|
174
195
|
* ```
|
|
175
196
|
*/
|
|
176
|
-
export const bindActionGlobally = createActionBinder(() => 'global')
|
|
197
|
+
export const bindActionGlobally = createActionBinder<unknown[]>(() => 'global')
|