@sanity/sdk 2.3.1 → 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.
@@ -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', () => {
@@ -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 = {...base[publishedId], _type: action.documentType, _id: draftId}
156
- const newDocWorking = {...working[publishedId], _type: action.documentType, _id: draftId}
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({
@@ -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 {bindActionByDataset} from '../store/createActionBinder'
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 = bindActionByDataset(
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 = bindActionByDataset(
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(keyFn: (config: SanityConfig) => string) {
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 unknown[], TReturn>(
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')