@sanity/sdk 0.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/index.d.ts +339 -0
  2. package/dist/index.js +492 -0
  3. package/dist/index.js.map +1 -0
  4. package/package.json +77 -0
  5. package/src/_exports/index.ts +39 -0
  6. package/src/auth/authStore.test.ts +296 -0
  7. package/src/auth/authStore.ts +125 -0
  8. package/src/auth/getAuthStore.test.ts +14 -0
  9. package/src/auth/getInternalAuthStore.ts +20 -0
  10. package/src/auth/internalAuthStore.test.ts +334 -0
  11. package/src/auth/internalAuthStore.ts +519 -0
  12. package/src/client/getClient.test.ts +41 -0
  13. package/src/client/getClient.ts +13 -0
  14. package/src/client/getSubscribableClient.test.ts +71 -0
  15. package/src/client/getSubscribableClient.ts +17 -0
  16. package/src/client/store/actions/getClientEvents.test.ts +95 -0
  17. package/src/client/store/actions/getClientEvents.ts +33 -0
  18. package/src/client/store/actions/getOrCreateClient.test.ts +56 -0
  19. package/src/client/store/actions/getOrCreateClient.ts +40 -0
  20. package/src/client/store/actions/receiveToken.test.ts +18 -0
  21. package/src/client/store/actions/receiveToken.ts +31 -0
  22. package/src/client/store/clientStore.test.ts +152 -0
  23. package/src/client/store/clientStore.ts +98 -0
  24. package/src/documentList/documentListStore.test.ts +575 -0
  25. package/src/documentList/documentListStore.ts +269 -0
  26. package/src/documents/.keep +0 -0
  27. package/src/instance/identity.test.ts +46 -0
  28. package/src/instance/identity.ts +28 -0
  29. package/src/instance/sanityInstance.test.ts +66 -0
  30. package/src/instance/sanityInstance.ts +64 -0
  31. package/src/instance/types.d.ts +29 -0
  32. package/src/schema/schemaStore.test.ts +30 -0
  33. package/src/schema/schemaStore.ts +32 -0
  34. package/src/store/createStore.test.ts +108 -0
  35. package/src/store/createStore.ts +106 -0
  36. package/src/tsdoc.json +39 -0
@@ -0,0 +1,575 @@
1
+ import {bufferTime, firstValueFrom, Observable, of} from 'rxjs'
2
+ import {describe, it, type Mock, vi} from 'vitest'
3
+
4
+ import {getClient} from '../client/getClient'
5
+ import {
6
+ createDocumentListStore,
7
+ type DocumentListState,
8
+ type DocumentListStore,
9
+ } from './documentListStore'
10
+
11
+ const mockClientUnsubscribe = vi.fn()
12
+
13
+ vi.mock('../client/store/clientStore', () => {
14
+ const unsubscribe = vi.fn()
15
+ const subscribe = vi.fn((observer) => {
16
+ observer.next({
17
+ id: 'mock-event-id',
18
+ type: 'message',
19
+ tags: [],
20
+ })
21
+ return {unsubscribe}
22
+ })
23
+
24
+ const mockClient = {
25
+ observable: {
26
+ fetch: vi.fn(() =>
27
+ of({
28
+ syncTags: [],
29
+ result: [],
30
+ }),
31
+ ),
32
+ },
33
+ live: {
34
+ events: vi.fn(() => {
35
+ const observable = of({
36
+ id: 'mock-event-id',
37
+ type: 'message',
38
+ tags: [],
39
+ })
40
+ // @ts-expect-error -- this is just a mock
41
+ observable.subscribe = subscribe
42
+ return observable
43
+ }),
44
+ },
45
+ }
46
+
47
+ const mockStore = {
48
+ subscribe: (callback: () => void) => {
49
+ callback()
50
+ return () => {}
51
+ },
52
+ }
53
+
54
+ const mockGetClientEvents = () => ({
55
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
56
+ subscribe: (subscriber: any) => {
57
+ subscriber.next(mockClient)
58
+ return {
59
+ unsubscribe: mockClientUnsubscribe,
60
+ }
61
+ },
62
+ })
63
+
64
+ return {
65
+ getClientStore: () => ({
66
+ store: mockStore,
67
+ getClientEvents: mockGetClientEvents,
68
+ getOrCreateClient: vi.fn(() => mockClient),
69
+ }),
70
+ }
71
+ })
72
+
73
+ describe('documentListStore', () => {
74
+ let documentListStore!: DocumentListStore
75
+ let client!: {
76
+ observable: {fetch: Mock}
77
+ live: {events: () => {subscribe: Mock}}
78
+ }
79
+
80
+ beforeEach(() => {
81
+ vi.clearAllMocks()
82
+ // @ts-expect-error the types are wrong here since we're mocking
83
+ client = getClient()
84
+
85
+ // @ts-expect-error the types are wrong here since we're mocking
86
+ documentListStore = createDocumentListStore()
87
+ })
88
+
89
+ function subscribeAndGetEmissions() {
90
+ return firstValueFrom(
91
+ new Observable<DocumentListState>((observer) => {
92
+ const subscription = documentListStore.subscribe(observer)
93
+ return () => subscription.unsubscribe()
94
+ }).pipe(bufferTime(100)),
95
+ )
96
+ }
97
+
98
+ function tick() {
99
+ return new Promise((resolve) => setTimeout(resolve, 0))
100
+ }
101
+
102
+ it('fetches a result set based on the set options', async () => {
103
+ client.observable.fetch.mockImplementation(() =>
104
+ of({
105
+ syncTags: [],
106
+ result: [
107
+ {_id: 'first-id', _type: 'author'},
108
+ {_id: 'second-id', _type: 'author'},
109
+ ],
110
+ }),
111
+ )
112
+
113
+ const emissionsPromise = subscribeAndGetEmissions()
114
+
115
+ documentListStore.setOptions({
116
+ filter: '_type == "author"',
117
+ })
118
+
119
+ const emissions = await emissionsPromise
120
+ expect(emissions).toHaveLength(3)
121
+
122
+ expect(emissions).toEqual([
123
+ {
124
+ filter: undefined,
125
+ isPending: false,
126
+ result: null,
127
+ sort: undefined,
128
+ },
129
+ {
130
+ filter: '_type == "author"',
131
+ isPending: true,
132
+ result: null,
133
+ sort: undefined,
134
+ },
135
+ {
136
+ filter: '_type == "author"',
137
+ isPending: false,
138
+ result: [
139
+ {_id: 'first-id', _type: 'author'},
140
+ {_id: 'second-id', _type: 'author'},
141
+ ],
142
+ sort: undefined,
143
+ },
144
+ ])
145
+ })
146
+
147
+ it('re-fetches the result set when the live content API emits a matching sync tag', async () => {
148
+ client.observable.fetch.mockImplementation(() =>
149
+ of({
150
+ syncTags: ['s1:example-sync-tag'],
151
+ result: [
152
+ {_id: 'first-id', _type: 'author'},
153
+ {_id: 'second-id', _type: 'author'},
154
+ ],
155
+ }),
156
+ )
157
+
158
+ expect(client.live.events().subscribe).toHaveBeenCalledTimes(1)
159
+ const [[observer]] = client.live.events().subscribe.mock.calls
160
+
161
+ documentListStore.setOptions({
162
+ filter: '_type == "author"',
163
+ })
164
+
165
+ const emissionsPromise = subscribeAndGetEmissions()
166
+
167
+ observer.next?.({id: 'event-id', tags: ['s1:example-sync-tag'], type: 'message'})
168
+ await tick()
169
+
170
+ const emissions = await emissionsPromise
171
+
172
+ expect(emissions).toHaveLength(3)
173
+
174
+ const result = [
175
+ {_id: 'first-id', _type: 'author'},
176
+ {_id: 'second-id', _type: 'author'},
177
+ ]
178
+
179
+ const filter = '_type == "author"'
180
+
181
+ expect(emissions).toEqual([
182
+ {isPending: false, result, filter},
183
+ {isPending: true, result, filter},
184
+ {isPending: false, result, filter},
185
+ ])
186
+ })
187
+
188
+ it('does not re-fetch on non-matching sync tags', async () => {
189
+ client.observable.fetch.mockImplementation(() =>
190
+ of({
191
+ syncTags: ['s1:example-sync-tag'],
192
+ result: [
193
+ {_id: 'first-id', _type: 'author'},
194
+ {_id: 'second-id', _type: 'author'},
195
+ ],
196
+ }),
197
+ )
198
+
199
+ expect(client.live.events().subscribe).toHaveBeenCalledTimes(1)
200
+ const [[observer]] = client.live.events().subscribe.mock.calls
201
+
202
+ documentListStore.setOptions({
203
+ filter: '_type == "author"',
204
+ })
205
+
206
+ const emissionsPromise = subscribeAndGetEmissions()
207
+
208
+ observer.next?.({id: 'event-id', tags: ['s1:no-match'], type: 'message'})
209
+ await tick()
210
+
211
+ const emissions = await emissionsPromise
212
+
213
+ expect(emissions).toHaveLength(1)
214
+ expect(emissions).toEqual([
215
+ {
216
+ isPending: false,
217
+ filter: '_type == "author"',
218
+ result: [
219
+ {_id: 'first-id', _type: 'author'},
220
+ {_id: 'second-id', _type: 'author'},
221
+ ],
222
+ },
223
+ ])
224
+ })
225
+
226
+ it('re-fetches the result set when options are changed', async () => {
227
+ const result = [
228
+ {_id: 'first-id', _type: 'author'},
229
+ {_id: 'second-id', _type: 'author'},
230
+ ]
231
+
232
+ client.observable.fetch.mockImplementation(() =>
233
+ of({
234
+ syncTags: [],
235
+ result: [
236
+ {_id: 'first-id', _type: 'author'},
237
+ {_id: 'second-id', _type: 'author'},
238
+ ],
239
+ }),
240
+ )
241
+
242
+ const emissionsPromise = subscribeAndGetEmissions()
243
+
244
+ documentListStore.setOptions({
245
+ filter: '_type == "author"',
246
+ })
247
+ await tick()
248
+ expect(documentListStore.getCurrent()).toEqual({
249
+ filter: '_type == "author"',
250
+ isPending: false,
251
+ result,
252
+ })
253
+
254
+ documentListStore.setOptions({
255
+ sort: [{direction: 'asc', field: 'name'}],
256
+ })
257
+ await tick()
258
+ expect(documentListStore.getCurrent()).toEqual({
259
+ filter: '_type == "author"',
260
+ sort: [{direction: 'asc', field: 'name'}],
261
+ isPending: false,
262
+ result,
263
+ })
264
+
265
+ documentListStore.setOptions({
266
+ filter: '_type == "book"',
267
+ })
268
+ await tick()
269
+ expect(documentListStore.getCurrent()).toEqual({
270
+ filter: '_type == "book"',
271
+ sort: [{direction: 'asc', field: 'name'}],
272
+ isPending: false,
273
+ result,
274
+ })
275
+
276
+ documentListStore.setOptions({
277
+ sort: [{direction: 'desc', field: 'name'}],
278
+ })
279
+ await tick()
280
+ expect(documentListStore.getCurrent()).toEqual({
281
+ filter: '_type == "book"',
282
+ sort: [{direction: 'desc', field: 'name'}],
283
+ isPending: false,
284
+ result,
285
+ })
286
+
287
+ const emissions = await emissionsPromise
288
+ expect(emissions).toHaveLength(9)
289
+
290
+ expect(emissions).toMatchObject([
291
+ {filter: undefined, isPending: false, result: null, sort: undefined},
292
+ {filter: '_type == "author"', isPending: true, result: null, sort: undefined},
293
+ {filter: '_type == "author"', isPending: false, result, sort: undefined},
294
+ {
295
+ filter: '_type == "author"',
296
+ isPending: true,
297
+ result,
298
+ sort: [{direction: 'asc', field: 'name'}],
299
+ },
300
+ {
301
+ filter: '_type == "author"',
302
+ isPending: false,
303
+ result,
304
+ sort: [{direction: 'asc', field: 'name'}],
305
+ },
306
+ {
307
+ filter: '_type == "book"',
308
+ isPending: true,
309
+ result,
310
+ sort: [{direction: 'asc', field: 'name'}],
311
+ },
312
+ {
313
+ filter: '_type == "book"',
314
+ isPending: false,
315
+ result,
316
+ sort: [{direction: 'asc', field: 'name'}],
317
+ },
318
+ {
319
+ filter: '_type == "book"',
320
+ isPending: true,
321
+ result,
322
+ sort: [{direction: 'desc', field: 'name'}],
323
+ },
324
+ {
325
+ filter: '_type == "book"',
326
+ isPending: false,
327
+ result,
328
+ sort: [{direction: 'desc', field: 'name'}],
329
+ },
330
+ ])
331
+
332
+ expect(client.observable.fetch).toHaveBeenCalledTimes(4)
333
+ expect(client.observable.fetch.mock.calls).toEqual([
334
+ [
335
+ '*[_type == "author"][0..$__limit]{_id, _type}',
336
+ {__limit: 50},
337
+ {
338
+ filterResponse: false,
339
+ lastLiveEventId: undefined,
340
+ returnQuery: false,
341
+ tag: 'sdk.document-list',
342
+ },
343
+ ],
344
+ [
345
+ '*[_type == "author"]| order(name asc)[0..$__limit]{_id, _type}',
346
+ {__limit: 50},
347
+ {
348
+ filterResponse: false,
349
+ lastLiveEventId: undefined,
350
+ returnQuery: false,
351
+ tag: 'sdk.document-list',
352
+ },
353
+ ],
354
+ [
355
+ '*[_type == "book"]| order(name asc)[0..$__limit]{_id, _type}',
356
+ {__limit: 50},
357
+ {
358
+ filterResponse: false,
359
+ lastLiveEventId: undefined,
360
+ returnQuery: false,
361
+ tag: 'sdk.document-list',
362
+ },
363
+ ],
364
+ [
365
+ '*[_type == "book"]| order(name desc)[0..$__limit]{_id, _type}',
366
+ {__limit: 50},
367
+ {
368
+ filterResponse: false,
369
+ lastLiveEventId: undefined,
370
+ returnQuery: false,
371
+ tag: 'sdk.document-list',
372
+ },
373
+ ],
374
+ ])
375
+ })
376
+
377
+ it('fetches more documents when load more is called', async () => {
378
+ const firstResult = [
379
+ {_id: 'first-id', _type: 'author'},
380
+ {_id: 'second-id', _type: 'author'},
381
+ ]
382
+ const secondResult = [
383
+ ...firstResult,
384
+ {_id: 'third-id', _type: 'author'},
385
+ {_id: 'fourth-id', _type: 'author'},
386
+ ]
387
+ client.observable.fetch.mockImplementationOnce(() =>
388
+ of({
389
+ syncTags: [],
390
+ result: firstResult,
391
+ }),
392
+ )
393
+
394
+ client.observable.fetch.mockImplementationOnce(() =>
395
+ of({
396
+ syncTags: [],
397
+ result: secondResult,
398
+ }),
399
+ )
400
+
401
+ documentListStore.setOptions({
402
+ filter: '_type == "author"',
403
+ })
404
+
405
+ const emissionsPromise = subscribeAndGetEmissions()
406
+
407
+ documentListStore.loadMore()
408
+ await tick()
409
+
410
+ const emissions = await emissionsPromise
411
+ expect(emissions).toHaveLength(3)
412
+ expect(emissions).toEqual([
413
+ {isPending: false, result: firstResult, filter: '_type == "author"'},
414
+ {isPending: true, result: firstResult, filter: '_type == "author"'},
415
+ {isPending: false, result: secondResult, filter: '_type == "author"'},
416
+ ])
417
+
418
+ expect(client.observable.fetch).toHaveBeenCalledTimes(2)
419
+ expect(client.observable.fetch.mock.calls).toEqual([
420
+ [
421
+ '*[_type == "author"][0..$__limit]{_id, _type}',
422
+ {__limit: 50},
423
+ {
424
+ filterResponse: false,
425
+ lastLiveEventId: undefined,
426
+ returnQuery: false,
427
+ tag: 'sdk.document-list',
428
+ },
429
+ ],
430
+ [
431
+ '*[_type == "author"][0..$__limit]{_id, _type}',
432
+ {__limit: 100},
433
+ {
434
+ filterResponse: false,
435
+ lastLiveEventId: undefined,
436
+ returnQuery: false,
437
+ tag: 'sdk.document-list',
438
+ },
439
+ ],
440
+ ])
441
+ })
442
+
443
+ it('unsubscribes from the live client when disposed', async () => {
444
+ client.observable.fetch.mockImplementation(() =>
445
+ of({
446
+ syncTags: [],
447
+ result: [
448
+ {_id: 'first-id', _type: 'author'},
449
+ {_id: 'second-id', _type: 'author'},
450
+ ],
451
+ }),
452
+ )
453
+
454
+ expect(mockClientUnsubscribe).not.toHaveBeenCalled()
455
+ documentListStore.dispose()
456
+ expect(mockClientUnsubscribe).toHaveBeenCalled()
457
+ })
458
+
459
+ it('preserves referential equality in the result set', async () => {
460
+ client.observable.fetch.mockImplementationOnce(() =>
461
+ of({
462
+ syncTags: [],
463
+ result: [
464
+ {_id: 'first-id', _type: 'author'},
465
+ {_id: 'second-id', _type: 'author'},
466
+ ],
467
+ }),
468
+ )
469
+
470
+ client.observable.fetch.mockImplementationOnce(() =>
471
+ of({
472
+ syncTags: [],
473
+ result: [{_id: 'first-id', _type: 'author'}],
474
+ }),
475
+ )
476
+
477
+ const emissionsPromise = subscribeAndGetEmissions()
478
+
479
+ documentListStore.setOptions({
480
+ filter: '_type == "author"',
481
+ })
482
+ await tick()
483
+ documentListStore.setOptions({
484
+ filter: '_id == "first-id"',
485
+ })
486
+
487
+ const emissions = await emissionsPromise
488
+ expect(emissions).toHaveLength(5)
489
+
490
+ expect(emissions).toEqual([
491
+ {
492
+ filter: undefined,
493
+ isPending: false,
494
+ result: null,
495
+ sort: undefined,
496
+ },
497
+ {
498
+ filter: '_type == "author"',
499
+ isPending: true,
500
+ result: null,
501
+ sort: undefined,
502
+ },
503
+ {
504
+ filter: '_type == "author"',
505
+ isPending: false,
506
+ result: [
507
+ {_id: 'first-id', _type: 'author'},
508
+ {_id: 'second-id', _type: 'author'},
509
+ ],
510
+ sort: undefined,
511
+ },
512
+ {
513
+ filter: '_id == "first-id"',
514
+ isPending: true,
515
+ result: [
516
+ {_id: 'first-id', _type: 'author'},
517
+ {_id: 'second-id', _type: 'author'},
518
+ ],
519
+ sort: undefined,
520
+ },
521
+ {
522
+ filter: '_id == "first-id"',
523
+ isPending: false,
524
+ result: [{_id: 'first-id', _type: 'author'}],
525
+ sort: undefined,
526
+ },
527
+ ])
528
+
529
+ const [{result: result1}, {result: result2}, {result: result3}] = emissions.slice(2)
530
+
531
+ expect(result1?.[0]).toBe(result2?.[0])
532
+ expect(result2?.[0]).toBe(result3?.[0])
533
+ })
534
+
535
+ it('fetches all documents when no filter is set', async () => {
536
+ const result = [
537
+ {_id: 'first-id', _type: 'author'},
538
+ {_id: 'second-id', _type: 'book'},
539
+ ]
540
+
541
+ client.observable.fetch.mockImplementation(() =>
542
+ of({
543
+ syncTags: [],
544
+ result,
545
+ }),
546
+ )
547
+
548
+ const emissionsPromise = subscribeAndGetEmissions()
549
+
550
+ // Don't set any filter
551
+ documentListStore.setOptions({})
552
+ await tick()
553
+
554
+ const emissions = await emissionsPromise
555
+ expect(emissions).toHaveLength(3)
556
+
557
+ expect(emissions).toEqual([
558
+ {filter: undefined, isPending: false, result: null, sort: undefined},
559
+ {filter: undefined, isPending: true, result: null, sort: undefined},
560
+ {filter: undefined, isPending: false, result, sort: undefined},
561
+ ])
562
+
563
+ expect(client.observable.fetch).toHaveBeenCalledTimes(1)
564
+ expect(client.observable.fetch.mock.calls[0]).toEqual([
565
+ '*[0..$__limit]{_id, _type}',
566
+ {__limit: 50},
567
+ {
568
+ filterResponse: false,
569
+ lastLiveEventId: undefined,
570
+ returnQuery: false,
571
+ tag: 'sdk.document-list',
572
+ },
573
+ ])
574
+ })
575
+ })