Skip to content

Commit e51e7af

Browse files
committed
feat: support source in document stores
1 parent 3018372 commit e51e7af

File tree

7 files changed

+163
-146
lines changed

7 files changed

+163
-146
lines changed

packages/core/src/document/documentStore.test.ts

Lines changed: 47 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ afterEach(() => {
8686
})
8787

8888
it('creates, edits, and publishes a document', async () => {
89-
const doc = createDocumentHandle({documentId: 'doc-single', documentType: 'article'})
89+
const doc = {documentId: 'doc-single', documentType: 'article', source}
9090
const documentState = getDocumentState(instance, doc)
9191

9292
// Initially the document is undefined
@@ -122,7 +122,7 @@ it('creates, edits, and publishes a document', async () => {
122122
})
123123

124124
it('edits existing documents', async () => {
125-
const doc = createDocumentHandle({documentId: 'existing-doc', documentType: 'article'})
125+
const doc = {documentId: 'existing-doc', documentType: 'article', source}
126126
const state = getDocumentState(instance, doc)
127127

128128
// not subscribed yet so the value is undefined
@@ -151,16 +151,16 @@ it('edits existing documents', async () => {
151151
})
152152

153153
it('sets optimistic changes synchronously', async () => {
154-
const doc = createDocumentHandle({documentId: 'optimistic', documentType: 'article'})
154+
const doc = {documentId: 'optimistic', documentType: 'article'}
155155

156-
const state1 = getDocumentState(instance1, doc)
157-
const state2 = getDocumentState(instance2, doc)
156+
const state1 = getDocumentState(instance1, {...doc, source: source1})
157+
const state2 = getDocumentState(instance2, {...doc, source: source2})
158158

159159
const unsubscribe1 = state1.subscribe()
160160
const unsubscribe2 = state2.subscribe()
161161

162162
// wait until the value is primed in the store
163-
await resolveDocument(instance1, doc)
163+
await resolveDocument(instance1, {...doc, source: source1})
164164

165165
// then the actions are synchronous
166166
expect(state1.getCurrent()).toBeNull()
@@ -204,9 +204,9 @@ it('sets optimistic changes synchronously', async () => {
204204
})
205205

206206
it('propagates changes between two instances', async () => {
207-
const doc = createDocumentHandle({documentId: 'doc-collab', documentType: 'article'})
208-
const state1 = getDocumentState(instance1, doc)
209-
const state2 = getDocumentState(instance2, doc)
207+
const doc = {documentId: 'doc-collab', documentType: 'article'}
208+
const state1 = getDocumentState(instance1, {...doc, source: source1})
209+
const state2 = getDocumentState(instance2, {...doc, source: source2})
210210

211211
const state1Unsubscribe = state1.subscribe()
212212
const state2Unsubscribe = state2.subscribe()
@@ -237,9 +237,9 @@ it('propagates changes between two instances', async () => {
237237
})
238238

239239
it('handles concurrent edits and resolves conflicts', async () => {
240-
const doc = createDocumentHandle({documentId: 'doc-concurrent', documentType: 'article'})
241-
const state1 = getDocumentState(instance1, doc)
242-
const state2 = getDocumentState(instance2, doc)
240+
const doc = {documentId: 'doc-concurrent', documentType: 'article'}
241+
const state1 = getDocumentState(instance1, {...doc, source: source1})
242+
const state2 = getDocumentState(instance2, {...doc, source: source2})
243243

244244
const state1Unsubscribe = state1.subscribe()
245245
const state2Unsubscribe = state2.subscribe()
@@ -281,7 +281,7 @@ it('handles concurrent edits and resolves conflicts', async () => {
281281
})
282282

283283
it('unpublishes and discards a document', async () => {
284-
const doc = createDocumentHandle({documentId: 'doc-pub-unpub', documentType: 'article'})
284+
const doc = {documentId: 'doc-pub-unpub', documentType: 'article', source}
285285
const documentState = getDocumentState(instance, doc)
286286
const unsubscribe = documentState.subscribe()
287287

@@ -312,7 +312,7 @@ it('unpublishes and discards a document', async () => {
312312
})
313313

314314
it('deletes a document', async () => {
315-
const doc = createDocumentHandle({documentId: 'doc-delete', documentType: 'article'})
315+
const doc = {documentId: 'doc-delete', documentType: 'article', source}
316316

317317
const documentState = getDocumentState(instance, doc)
318318
const unsubscribe = documentState.subscribe()
@@ -333,7 +333,7 @@ it('deletes a document', async () => {
333333
})
334334

335335
it('cleans up document state when there are no subscribers', async () => {
336-
const doc = createDocumentHandle({documentId: 'doc-cleanup', documentType: 'article'})
336+
const doc = {documentId: 'doc-cleanup', documentType: 'article', source}
337337
const documentState = getDocumentState(instance, doc)
338338

339339
// Subscribe to the document state.
@@ -356,7 +356,7 @@ it('cleans up document state when there are no subscribers', async () => {
356356
})
357357

358358
it('fetches documents if there are no active subscriptions for the actions applied', async () => {
359-
const doc = createDocumentHandle({documentId: 'existing-doc', documentType: 'article'})
359+
const doc = {documentId: 'existing-doc', documentType: 'article', source}
360360

361361
const {getCurrent} = getDocumentState(instance, doc)
362362
expect(getCurrent()).toBeUndefined()
@@ -411,7 +411,7 @@ it('fetches documents if there are no active subscriptions for the actions appli
411411
})
412412

413413
it('batches edit transaction into one outgoing transaction', async () => {
414-
const doc = createDocumentHandle({documentId: crypto.randomUUID(), documentType: 'article'})
414+
const doc = {documentId: crypto.randomUUID(), documentType: 'article', source}
415415

416416
const unsubscribe = getDocumentState(instance, doc).subscribe()
417417

@@ -438,7 +438,7 @@ it('batches edit transaction into one outgoing transaction', async () => {
438438
})
439439

440440
it('provides the consistency status via `getDocumentSyncStatus`', async () => {
441-
const doc = createDocumentHandle({documentId: crypto.randomUUID(), documentType: 'article'})
441+
const doc = {documentId: crypto.randomUUID(), documentType: 'article', source}
442442

443443
const syncStatus = getDocumentSyncStatus(instance, doc)
444444
expect(syncStatus.getCurrent()).toBeUndefined()
@@ -482,15 +482,18 @@ it('reverts failed outgoing transaction locally', async () => {
482482
})
483483

484484
const revertedEventPromise = new Promise<TransactionRevertedEvent>((resolve) => {
485-
const unsubscribe = subscribeDocumentEvents(instance, (e) => {
486-
if (e.type === 'reverted') {
487-
resolve(e)
488-
unsubscribe()
489-
}
485+
const unsubscribe = subscribeDocumentEvents(instance, {
486+
onEvent: (e) => {
487+
if (e.type === 'reverted') {
488+
resolve(e)
489+
unsubscribe()
490+
}
491+
},
492+
source,
490493
})
491494
})
492495

493-
const doc = createDocumentHandle({documentId: crypto.randomUUID(), documentType: 'article'})
496+
const doc = {documentId: crypto.randomUUID(), documentType: 'article', source}
494497

495498
const {getCurrent, subscribe} = getDocumentState(instance, doc)
496499
const unsubscribe = subscribe()
@@ -546,15 +549,18 @@ it('reverts failed outgoing transaction locally', async () => {
546549

547550
it('removes a queued transaction if it fails to apply', async () => {
548551
const actionErrorEventPromise = new Promise<ActionErrorEvent>((resolve) => {
549-
const unsubscribe = subscribeDocumentEvents(instance, (e) => {
550-
if (e.type === 'error') {
551-
resolve(e)
552-
unsubscribe()
553-
}
552+
const unsubscribe = subscribeDocumentEvents(instance, {
553+
onEvent: (e) => {
554+
if (e.type === 'error') {
555+
resolve(e)
556+
unsubscribe()
557+
}
558+
},
559+
source,
554560
})
555561
})
556562

557-
const doc = createDocumentHandle({documentId: crypto.randomUUID(), documentType: 'article'})
563+
const doc = {documentId: crypto.randomUUID(), documentType: 'article', source}
558564
const state = getDocumentState(instance, doc)
559565
const unsubscribe = state.subscribe()
560566

@@ -587,10 +593,11 @@ it('returns allowed true when no permission errors occur', async () => {
587593
client.observable.request = vi.fn().mockReturnValue(of(datasetAcl))
588594

589595
// Create a document and subscribe to it.
590-
const doc = createDocumentHandle({
596+
const doc = {
591597
documentId: 'doc-perm-allowed',
592598
documentType: 'article',
593-
})
599+
source,
600+
}
594601
const state = getDocumentState(instance, doc)
595602
const unsubscribe = state.subscribe()
596603
await applyDocumentActions(instance, {actions: [createDocument(doc)], source}).then((r) =>
@@ -606,6 +613,7 @@ it('returns allowed true when no permission errors occur', async () => {
606613
patches: [{set: {title: 'New Title'}}],
607614
},
608615
],
616+
source,
609617
})
610618
// Wait briefly to allow permissions calculation.
611619
await new Promise((resolve) => setTimeout(resolve, 10))
@@ -638,7 +646,7 @@ it('returns allowed false with reasons when permission errors occur', async () =
638646
vi.mocked(client.request).mockResolvedValue(datasetAcl)
639647

640648
const doc = createDocumentHandle({documentId: 'doc-perm-denied', documentType: 'article'})
641-
const result = await resolvePermissions(instance, {actions: [createDocument(doc)]})
649+
const result = await resolvePermissions(instance, {actions: [createDocument(doc)], source})
642650

643651
const message = 'You do not have permission to create a draft for document "doc-perm-denied".'
644652
expect(result).toMatchObject({
@@ -659,17 +667,19 @@ it('fetches dataset ACL and updates grants in the document store state', async (
659667
const book = createDocumentHandle({documentId: crypto.randomUUID(), documentType: 'book'})
660668
const author = createDocumentHandle({documentId: crypto.randomUUID(), documentType: 'author'})
661669

662-
expect(await resolvePermissions(instance, {actions: [createDocument(book)]})).toEqual({
670+
expect(await resolvePermissions(instance, {actions: [createDocument(book)], source})).toEqual({
663671
allowed: true,
664672
})
665-
expect(await resolvePermissions(instance, {actions: [createDocument(author)]})).toMatchObject({
673+
expect(
674+
await resolvePermissions(instance, {actions: [createDocument(author)], source}),
675+
).toMatchObject({
666676
allowed: false,
667677
message: expect.stringContaining('You do not have permission to create a draft for document'),
668678
})
669679
})
670680

671681
it('returns a promise that resolves when a document has been loaded in the store (useful for suspense)', async () => {
672-
const doc = createDocumentHandle({documentId: crypto.randomUUID(), documentType: 'article'})
682+
const doc = {documentId: crypto.randomUUID(), documentType: 'article', source}
673683

674684
expect(await resolveDocument(instance, doc)).toBe(null)
675685

@@ -691,7 +701,7 @@ it('returns a promise that resolves when a document has been loaded in the store
691701

692702
it('emits an event for each action after an outgoing transaction has been accepted', async () => {
693703
const handler = vi.fn()
694-
const unsubscribe = subscribeDocumentEvents(instance, handler)
704+
const unsubscribe = subscribeDocumentEvents(instance, {onEvent: handler, source})
695705

696706
const documentId = crypto.randomUUID()
697707
const doc = createDocumentHandle({documentId, documentType: 'article'})

packages/core/src/document/documentStore.ts

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import {
2727
} from 'rxjs'
2828

2929
import {getClientState} from '../client/clientStore'
30-
import {type DocumentHandle} from '../config/sanityConfig'
30+
import {type DocumentHandle, type DocumentSource} from '../config/sanityConfig'
3131
import {
3232
bindActionByDataset,
3333
type BoundDatasetKey,
@@ -138,13 +138,11 @@ export const documentStore = defineStore<DocumentStoreState, BoundDatasetKey>({
138138
* @beta
139139
* Options for specifying a document and optionally a path within it.
140140
*/
141-
export interface DocumentOptions<
142-
TPath extends string | undefined = undefined,
143-
TDocumentType extends string = string,
144-
TDataset extends string = string,
145-
TProjectId extends string = string,
146-
> extends DocumentHandle<TDocumentType, TDataset, TProjectId> {
141+
export interface DocumentOptions<TPath extends string | undefined = undefined> {
147142
path?: TPath
143+
documentId: string
144+
documentType: string
145+
source: DocumentSource
148146
}
149147

150148
/** @beta */
@@ -154,7 +152,7 @@ export function getDocumentState<
154152
TProjectId extends string = string,
155153
>(
156154
instance: SanityInstance,
157-
options: DocumentOptions<undefined, TDocumentType, TDataset, TProjectId>,
155+
options: DocumentOptions<undefined>,
158156
): StateSource<SanityDocument<TDocumentType, `${TProjectId}.${TDataset}`> | undefined | null>
159157

160158
/** @beta */
@@ -165,7 +163,7 @@ export function getDocumentState<
165163
TProjectId extends string = string,
166164
>(
167165
instance: SanityInstance,
168-
options: DocumentOptions<TPath, TDocumentType, TDataset, TProjectId>,
166+
options: DocumentOptions<TPath>,
169167
): StateSource<
170168
JsonMatch<SanityDocument<TDocumentType, `${TProjectId}.${TDataset}`>, TPath> | undefined
171169
>
@@ -215,12 +213,12 @@ export function resolveDocument<
215213
TProjectId extends string = string,
216214
>(
217215
instance: SanityInstance,
218-
docHandle: DocumentHandle<TDocumentType, TDataset, TProjectId>,
216+
docHandle: Omit<DocumentOptions, 'path'>,
219217
): Promise<SanityDocument<TDocumentType, `${TProjectId}.${TDataset}`> | null>
220218
/** @beta */
221219
export function resolveDocument<TData extends SanityDocument>(
222220
instance: SanityInstance,
223-
docHandle: DocumentHandle<string, string, string>,
221+
docHandle: Omit<DocumentOptions, 'path'>,
224222
): Promise<TData | null>
225223
/** @beta */
226224
export function resolveDocument(
@@ -230,12 +228,9 @@ export function resolveDocument(
230228
}
231229
const _resolveDocument = bindActionByDataset(
232230
documentStore,
233-
({instance}, docHandle: DocumentHandle<string, string, string>) => {
231+
({instance}, docHandle: Omit<DocumentOptions, 'path'>) => {
234232
return firstValueFrom(
235-
getDocumentState(instance, {
236-
...docHandle,
237-
path: undefined,
238-
}).observable.pipe(filter((i) => i !== undefined)),
233+
getDocumentState(instance, docHandle).observable.pipe(filter((i) => i !== undefined)),
239234
) as Promise<SanityDocument | null>
240235
},
241236
)
@@ -246,9 +241,9 @@ export const getDocumentSyncStatus = bindActionByDataset(
246241
createStateSourceAction({
247242
selector: (
248243
{state: {error, documentStates: documents, outgoing, applied, queued}},
249-
doc: DocumentHandle,
244+
doc: Omit<DocumentOptions, 'path'>,
250245
) => {
251-
const documentId = typeof doc === 'string' ? doc : doc.documentId
246+
const documentId = doc.documentId
252247
if (error) throw error
253248
const draftId = getDraftId(documentId)
254249
const publishedId = getPublishedId(documentId)
@@ -265,6 +260,7 @@ export const getDocumentSyncStatus = bindActionByDataset(
265260

266261
type PermissionsStateOptions = {
267262
actions: DocumentAction[]
263+
source: DocumentSource
268264
}
269265

270266
/** @beta */
@@ -294,9 +290,9 @@ export const resolvePermissions = bindActionByDataset(
294290
/** @beta */
295291
export const subscribeDocumentEvents = bindActionByDataset(
296292
documentStore,
297-
({state}, eventHandler: (e: DocumentEvent) => void) => {
293+
({state}, {onEvent}: {onEvent: (e: DocumentEvent) => void; source: DocumentSource}) => {
298294
const {events} = state.get()
299-
const subscription = events.subscribe(eventHandler)
295+
const subscription = events.subscribe(onEvent)
300296
return () => subscription.unsubscribe()
301297
},
302298
)

0 commit comments

Comments
 (0)